<!DOCTYPE html>
<html lang="en-US" dir="ltr">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>高并发，你真的理解透彻了吗 | VitePress</title>
    <meta name="description" content="A VitePress site">
    <link rel="preload stylesheet" href="/notebook/assets/style.3dbfd0c2.css" as="style">
    
    <script type="module" src="/notebook/assets/app.8aaa4cbe.js"></script>
    <link rel="preload" href="/notebook/assets/inter-roman-latin.2ed14f66.woff2" as="font" type="font/woff2" crossorigin="">
    <link rel="modulepreload" href="/notebook/assets/chunks/framework.1336c4e5.js">
    <link rel="modulepreload" href="/notebook/assets/chunks/theme.20cddc0c.js">
    <link rel="modulepreload" href="/notebook/assets/三高_高并发.md.ea9ffc99.lean.js">
    <script id="check-dark-light">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
  </head>
  <body>
    <div id="app"><div class="Layout" data-v-255ec12d><!--[--><!--]--><!--[--><span tabindex="-1" data-v-ae3e3f51></span><a href="#VPContent" class="VPSkipLink visually-hidden" data-v-ae3e3f51> Skip to content </a><!--]--><!----><header class="VPNav" data-v-255ec12d data-v-7e5bc4a5><div class="VPNavBar has-sidebar" data-v-7e5bc4a5 data-v-0937f67c><div class="container" data-v-0937f67c><div class="title" data-v-0937f67c><div class="VPNavBarTitle has-sidebar" data-v-0937f67c data-v-86d1bed8><a class="title" href="/notebook/" data-v-86d1bed8><!--[--><!--]--><!--[--><img class="VPImage logo" src="/notebook/Vue.png" alt data-v-8426fc1a><!--]--><!--[-->任硕的文档<!--]--><!--[--><!--]--></a></div></div><div class="content" data-v-0937f67c><div class="curtain" data-v-0937f67c></div><div class="content-body" data-v-0937f67c><!--[--><!--]--><div class="VPNavBarSearch search" style="--vp-meta-key:&#39;Meta&#39;;" data-v-0937f67c><!--[--><!----><div id="docsearch"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><svg class="DocSearch-Search-Icon" width="20" height="20" viewBox="0 0 20 20" aria-label="search icon"><path d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button></div><!--]--></div><nav aria-labelledby="main-nav-aria-label" class="VPNavBarMenu menu" data-v-0937f67c data-v-7f418b0f><span id="main-nav-aria-label" class="visually-hidden" data-v-7f418b0f>Main Navigation</span><!--[--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>Java学前端</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/HTML+JS.html" data-v-2f2cfafc><!--[-->HTML+JS<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/CSS.html" data-v-2f2cfafc><!--[-->CSS<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/Vue2+%E7%BB%84%E4%BB%B6.html" data-v-2f2cfafc><!--[-->Vue2+组件<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/Vue3+%E7%BB%84%E4%BB%B6.html" data-v-2f2cfafc><!--[-->Vue3+组件<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/Java%E5%AD%A6%E5%89%8D%E7%AB%AF/React.html" data-v-2f2cfafc><!--[-->React<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>软件测试</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E8%BD%AF%E4%BB%B6%E6%B5%8B%E8%AF%95/%E6%B5%8B%E8%AF%95%E5%9F%BA%E7%A1%80.html" data-v-2f2cfafc><!--[-->测试基础<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E8%BD%AF%E4%BB%B6%E6%B5%8B%E8%AF%95/%E5%8E%8B%E5%8A%9B%E6%B5%8B%E8%AF%95.html" data-v-2f2cfafc><!--[-->压力测试<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>多线程</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E5%B9%B6%E5%8F%91%20&amp;%20%E5%A4%9A%E7%BA%BF%E7%A8%8B/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-2f2cfafc><!--[-->基础篇<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E5%B9%B6%E5%8F%91%20&amp;%20%E5%A4%9A%E7%BA%BF%E7%A8%8B/%E5%B9%B6%E5%8F%91%E5%AE%8C%E5%96%84.html" data-v-2f2cfafc><!--[-->进阶篇<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>开发工具</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/Chrome.html" data-v-2f2cfafc><!--[-->Chrome<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/IDEA%E5%9F%BA%E7%A1%80.html" data-v-2f2cfafc><!--[-->IDEA基础<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/IDEA%E6%8F%92%E4%BB%B6.html" data-v-2f2cfafc><!--[-->IDEA插件<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/IDEA/VS%20Code.html" data-v-2f2cfafc><!--[-->VS Code<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--[--><div class="VPFlyout VPNavBarMenuGroup" data-v-7f418b0f data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" data-v-a7b5672a><span class="text" data-v-a7b5672a><!----><span data-v-a7b5672a>消息中间件</span><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="text-icon" data-v-a7b5672a><path d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"></path></svg></span></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><div class="items" data-v-e7ea1737><!--[--><!--[--><div class="VPMenuGroup" data-v-e7ea1737 data-v-69e747b5><!----><!--[--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/RabbitMQ.html" data-v-2f2cfafc><!--[-->RabbitMQ<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/RocketMQ.html" data-v-2f2cfafc><!--[-->RocketMQ<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/Kafka.html" data-v-2f2cfafc><!--[-->Kafka<!--]--></a></div><!--]--><!--[--><div class="VPMenuLink" data-v-69e747b5 data-v-2f2cfafc><a class="VPLink link" href="/notebook/%E6%B6%88%E6%81%AF%E4%B8%AD%E9%97%B4%E4%BB%B6/Canal.html" data-v-2f2cfafc><!--[-->Canal<!--]--></a></div><!--]--><!--]--></div><!--]--><!--]--></div><!--[--><!--]--></div></div></div><!--]--><!--]--></nav><!----><div class="VPNavBarAppearance appearance" data-v-0937f67c data-v-f6a63727><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title="toggle dark mode" aria-checked="false" data-v-f6a63727 data-v-82b282f1 data-v-f3c41672><span class="check" data-v-f3c41672><span class="icon" data-v-f3c41672><!--[--><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="sun" data-v-82b282f1><path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z"></path><path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z"></path><path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z"></path><path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z"></path><path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z"></path><path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z"></path><path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z"></path><path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z"></path><path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="moon" data-v-82b282f1><path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"></path></svg><!--]--></span></span></button></div><div class="VPSocialLinks VPNavBarSocialLinks social-links" data-v-0937f67c data-v-0394ad82 data-v-7bc22406><!--[--><a class="VPSocialLink no-icon" href="https://github.com/renshuo123/renshuo123.github.io" aria-label="github" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a><a class="VPSocialLink no-icon" href="#" aria-label="twitter" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></a><a class="VPSocialLink no-icon" href="https://github.com/" aria-label target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg t="1676028692954" class="icon" ...</path></svg></a><!--]--></div><div class="VPFlyout VPNavBarExtra extra" data-v-0937f67c data-v-40855f84 data-v-a7b5672a><button type="button" class="button" aria-haspopup="true" aria-expanded="false" aria-label="extra navigation" data-v-a7b5672a><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="icon" data-v-a7b5672a><circle cx="12" cy="12" r="2"></circle><circle cx="19" cy="12" r="2"></circle><circle cx="5" cy="12" r="2"></circle></svg></button><div class="menu" data-v-a7b5672a><div class="VPMenu" data-v-a7b5672a data-v-e7ea1737><!----><!--[--><!--[--><!----><div class="group" data-v-40855f84><div class="item appearance" data-v-40855f84><p class="label" data-v-40855f84>Appearance</p><div class="appearance-action" data-v-40855f84><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title="toggle dark mode" aria-checked="false" data-v-40855f84 data-v-82b282f1 data-v-f3c41672><span class="check" data-v-f3c41672><span class="icon" data-v-f3c41672><!--[--><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="sun" data-v-82b282f1><path d="M12,18c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S15.3,18,12,18zM12,8c-2.2,0-4,1.8-4,4c0,2.2,1.8,4,4,4c2.2,0,4-1.8,4-4C16,9.8,14.2,8,12,8z"></path><path d="M12,4c-0.6,0-1-0.4-1-1V1c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,3.6,12.6,4,12,4z"></path><path d="M12,24c-0.6,0-1-0.4-1-1v-2c0-0.6,0.4-1,1-1s1,0.4,1,1v2C13,23.6,12.6,24,12,24z"></path><path d="M5.6,6.6c-0.3,0-0.5-0.1-0.7-0.3L3.5,4.9c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C6.2,6.5,5.9,6.6,5.6,6.6z"></path><path d="M19.8,20.8c-0.3,0-0.5-0.1-0.7-0.3l-1.4-1.4c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l1.4,1.4c0.4,0.4,0.4,1,0,1.4C20.3,20.7,20,20.8,19.8,20.8z"></path><path d="M3,13H1c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S3.6,13,3,13z"></path><path d="M23,13h-2c-0.6,0-1-0.4-1-1s0.4-1,1-1h2c0.6,0,1,0.4,1,1S23.6,13,23,13z"></path><path d="M4.2,20.8c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C4.7,20.7,4.5,20.8,4.2,20.8z"></path><path d="M18.4,6.6c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l1.4-1.4c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-1.4,1.4C18.9,6.5,18.6,6.6,18.4,6.6z"></path></svg><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="moon" data-v-82b282f1><path d="M12.1,22c-0.3,0-0.6,0-0.9,0c-5.5-0.5-9.5-5.4-9-10.9c0.4-4.8,4.2-8.6,9-9c0.4,0,0.8,0.2,1,0.5c0.2,0.3,0.2,0.8-0.1,1.1c-2,2.7-1.4,6.4,1.3,8.4c2.1,1.6,5,1.6,7.1,0c0.3-0.2,0.7-0.3,1.1-0.1c0.3,0.2,0.5,0.6,0.5,1c-0.2,2.7-1.5,5.1-3.6,6.8C16.6,21.2,14.4,22,12.1,22zM9.3,4.4c-2.9,1-5,3.6-5.2,6.8c-0.4,4.4,2.8,8.3,7.2,8.7c2.1,0.2,4.2-0.4,5.8-1.8c1.1-0.9,1.9-2.1,2.4-3.4c-2.5,0.9-5.3,0.5-7.5-1.1C9.2,11.4,8.1,7.7,9.3,4.4z"></path></svg><!--]--></span></span></button></div></div></div><div class="group" data-v-40855f84><div class="item social-links" data-v-40855f84><div class="VPSocialLinks social-links-list" data-v-40855f84 data-v-7bc22406><!--[--><a class="VPSocialLink no-icon" href="https://github.com/renshuo123/renshuo123.github.io" aria-label="github" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a><a class="VPSocialLink no-icon" href="#" aria-label="twitter" target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Twitter</title><path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/></svg></a><a class="VPSocialLink no-icon" href="https://github.com/" aria-label target="_blank" rel="noopener" data-v-7bc22406 data-v-f80f8133><svg t="1676028692954" class="icon" ...</path></svg></a><!--]--></div></div></div><!--]--><!--]--></div></div></div><!--[--><!--]--><button type="button" class="VPNavBarHamburger hamburger" aria-label="mobile navigation" aria-expanded="false" aria-controls="VPNavScreen" data-v-0937f67c data-v-e5dd9c1c><span class="container" data-v-e5dd9c1c><span class="top" data-v-e5dd9c1c></span><span class="middle" data-v-e5dd9c1c></span><span class="bottom" data-v-e5dd9c1c></span></span></button></div></div></div></div><!----></header><div class="VPLocalNav reached-top" data-v-255ec12d data-v-5cfd5582><button class="menu" aria-expanded="false" aria-controls="VPSidebarNav" data-v-5cfd5582><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="menu-icon" data-v-5cfd5582><path d="M17,11H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,11,17,11z"></path><path d="M21,7H3C2.4,7,2,6.6,2,6s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,7,21,7z"></path><path d="M21,15H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h18c0.6,0,1,0.4,1,1S21.6,15,21,15z"></path><path d="M17,19H3c-0.6,0-1-0.4-1-1s0.4-1,1-1h14c0.6,0,1,0.4,1,1S17.6,19,17,19z"></path></svg><span class="menu-text" data-v-5cfd5582>Menu</span></button><div class="VPLocalNavOutlineDropdown" style="--vp-vh:0px;" data-v-5cfd5582 data-v-18201f51><button data-v-18201f51>Return to top</button><!----></div></div><aside class="VPSidebar" data-v-255ec12d data-v-845b8fc6><div class="curtain" data-v-845b8fc6></div><nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1" data-v-845b8fc6><span class="visually-hidden" id="sidebar-aria-label" data-v-845b8fc6> Sidebar Navigation </span><!--[--><!--]--><!--[--><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Java</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E6%96%B0%E7%89%B9%E6%80%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java新特性</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E9%9B%86%E5%90%88.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java集合</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Java/Java%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Java高级</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Linux</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/Linux%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Linux基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/Linux%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Linux新特性</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/Shell.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Shell脚本</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/%E5%AE%9E%E7%94%A8%E8%84%9A%E6%9C%AC.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>实用脚本</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Linux/%E8%BD%AF%E4%BB%B6%E9%83%A8%E7%BD%B2.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>软件部署</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Nginx</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E5%AE%9E%E6%88%98%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>实战篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Nginx/%E9%9D%A2%E8%AF%95%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>面试篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SSM</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/Maven.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Maven</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/Spring.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Spring</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/SpringMVC.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringMVC</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/SSM/SpringBatch.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringBatch</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SpringBoot</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E5%BA%94%E7%94%A8%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>应用篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E6%96%B0%E7%89%B9%E6%80%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>新特性</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/3%E3%80%81SpringBoot/%E8%BF%90%E7%BB%B4&amp;%E5%8E%9F%E7%90%86.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>运维&原理</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SpringCloud</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringCloud</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/%E5%BF%85%E5%A4%87/Sentinel.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Sentinel</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>SpringSecurity</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/SpringSecurity/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringSecurity基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/SpringSecurity/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringSecurity进阶篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/SpringSecurity/%E9%AB%98%E7%BA%A7%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>SpringSecurity高级篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Mybatis & MybatisPlus</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Mybatis&amp;MybatisPlus/Mybatis.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Mybatis</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Mybatis&amp;MybatisPlus/MybatisPlus.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MybatisPlus</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/Mybatis&amp;MybatisPlus/JPA.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>JPA</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>Git & ChatGPT</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Git.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Git</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Github.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Github</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/ChatGPT.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ChatGPT</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Jenkins.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Jenkins</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/5%E3%80%81%E8%BF%90%E7%BB%B4/Netty.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Netty</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>数据库</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>MySQL</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E4%BC%98%E5%8C%96.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL优化</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E8%AE%BE%E8%AE%A1.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL设计</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/MySQL%E6%A0%B8%E5%BF%83/%E8%BF%90%E7%BB%B4.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MySQL运维</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>分库分表</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>Redis</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E4%BC%98%E5%8C%96.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis优化</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%8E%9F%E7%90%86.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis原理</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis高级</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%AE%9E%E6%88%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Redis实战</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/%E6%9C%AC%E5%9C%B0%E7%BC%93%E5%AD%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>本地缓存</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>MongoDB</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MongoDB/%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MongoDB基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/MongoDB/%E6%95%B4%E5%90%88.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>MongoDB进阶</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>ElasticSearch</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/ElasticSearch/1%E3%80%81ES%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/ElasticSearch/3%E3%80%81ES%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES高级</p><!--]--></a><!----></div><!----></div><!--]--></div></section><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/influxdb.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>InfluxDB</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/2%E3%80%81%E6%95%B0%E6%8D%AE%E5%BA%93/Neo4j.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Neo4j</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible has-active" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>高并发 & 秒杀 & 分布式</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E5%88%86%E5%B8%83%E5%BC%8F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>分布式理论</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/4%E3%80%81%E5%BE%AE%E6%9C%8D%E5%8A%A1/%E5%BF%85%E5%A4%87/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>分布式锁</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E7%A7%92%E6%9D%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>秒杀</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E9%AB%98%E5%8F%AF%E7%94%A8.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>高可用</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%B8%89%E9%AB%98/%E9%AB%98%E5%B9%B6%E5%8F%91.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>高并发</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>云原生</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%BA%91%E5%8E%9F%E7%94%9F/Docker.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Docker</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E4%BA%91%E5%8E%9F%E7%94%9F/K8S.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>K8S</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>可视化 & 监控</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/%E7%9B%91%E6%8E%A7%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>监控基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/%E7%9B%91%E6%8E%A7%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>监控进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/%E5%8F%AF%E8%A7%86%E5%8C%96%E5%A4%A7%E5%B1%8F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>可视化大屏</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E5%8F%AF%E8%A7%86%E5%8C%96%20&amp;%20%E7%9B%91%E6%8E%A7/Zabbix.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Zabbix</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>学前端</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>HTML+CSS</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/1%E3%80%81HTML+CSS/HTML%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>HTML基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/1%E3%80%81HTML+CSS/CSS%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>CSS基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/1%E3%80%81HTML+CSS/%E7%BD%91%E9%A1%B5%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>网页进阶</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>JS+TS</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/JS%20%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>JS基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/JS%20%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>JS进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/ES6%20%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES6基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/ES6%20%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>ES6进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/2%E3%80%81JS+TS/TypeScript.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>TS基础</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>NodeJS</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/4%E3%80%81Node/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Node基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/4%E3%80%81Node/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Node进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/4%E3%80%81Node/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目实战</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>Vue</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue3/Vue3%E8%BF%9B%E9%98%B6.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Vue3进阶</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue3/Vue3%E9%AB%98%E7%BA%A7.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Vue3高级</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue3/Vue3%E6%96%B0%E8%AF%AD%E6%B3%95.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>Vue3新语法</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/3%E3%80%81Vue/Vue2/Vue2%E9%A1%B9%E7%9B%AE.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目实战</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>小程序</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>小程序基础</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E4%BC%98%E5%8C%96.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>小程序优化</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/uniapp.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>uniapp</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/1%E3%80%81%E5%AD%A6%E5%89%8D%E7%AB%AF/5%E3%80%81%E5%B0%8F%E7%A8%8B%E5%BA%8F/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A1%B9%E7%9B%AE.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目实战</p><!--]--></a><!----></div><!----></div><!--]--></div></section><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>计算机基础</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>数据结构</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>操作系统</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>设计模式</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>计算机网络</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/UML.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>UML</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9F%BA%E7%A1%80/%E7%AE%97%E6%B3%95/LeetCode.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>LeetCode</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0 collapsible collapsed" data-v-845b8fc6 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h2 class="text" data-v-9b797284>项目实战</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>云尚办公</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E4%BA%91%E5%B0%9A%E5%8A%9E%E5%85%AC/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>小兔鲜</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E5%B0%8F%E5%85%94%E9%B2%9C/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E5%B0%8F%E5%85%94%E9%B2%9C/%E8%BF%9B%E9%98%B6%E7%AF%871.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇1</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E5%B0%8F%E5%85%94%E9%B2%9C/%E8%BF%9B%E9%98%B6%E7%AF%872.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇2</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>地图</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E7%99%BE%E5%BA%A6%E5%9C%B0%E5%9B%BE/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E7%99%BE%E5%BA%A6%E5%9C%B0%E5%9B%BE/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>苍穹外卖</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E8%8B%8D%E7%A9%B9%E5%A4%96%E5%8D%96/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><section class="VPSidebarItem level-1 collapsible collapsed" data-v-9b797284 data-v-9b797284><div class="item" role="button" tabindex="0" data-v-9b797284><div class="indicator" data-v-9b797284></div><h3 class="text" data-v-9b797284>黑马头条</h3><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-9b797284><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewbox="0 0 24 24" class="caret-icon" data-v-9b797284><path d="M9,19c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l5.3-5.3L8.3,6.7c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6,6c0.4,0.4,0.4,1,0,1.4l-6,6C9.5,18.9,9.3,19,9,19z"></path></svg></div></div><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E5%9F%BA%E7%A1%80%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>基础篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E8%BF%9B%E9%98%B6%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E8%BF%9B%E9%98%B6%E7%AF%872.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>进阶篇2</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-2 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%BB%91%E9%A9%AC%E5%A4%B4%E6%9D%A1/%E9%AB%98%E7%BA%A7%E7%AF%87.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>高级篇</p><!--]--></a><!----></div><!----></div><!--]--></div></section><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E6%94%AF%E4%BB%98.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>支付</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98/%E9%A1%B9%E7%9B%AE%E6%8E%A8%E8%8D%90.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>项目推荐</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="group" data-v-845b8fc6><section class="VPSidebarItem level-0" data-v-845b8fc6 data-v-9b797284><!----><div class="items" data-v-9b797284><!--[--><div class="VPSidebarItem level-1 is-link" data-v-9b797284 data-v-9b797284><div class="item" data-v-9b797284><div class="indicator" data-v-9b797284></div><a class="VPLink link link" href="/notebook/team.html" data-v-9b797284><!--[--><p class="text" data-v-9b797284>团队成员</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><!--]--><!--[--><!--]--></nav></aside><div class="VPContent has-sidebar" id="VPContent" data-v-255ec12d data-v-669faec9><div class="VPDoc has-sidebar has-aside" data-v-669faec9 data-v-6b87e69f><!--[--><!--]--><div class="container" data-v-6b87e69f><div class="aside" data-v-6b87e69f><div class="aside-curtain" data-v-6b87e69f></div><div class="aside-container" data-v-6b87e69f><div class="aside-content" data-v-6b87e69f><div class="VPDocAside" data-v-6b87e69f data-v-3f215769><!--[--><!--]--><!--[--><!--]--><div class="VPDocAsideOutline" data-v-3f215769 data-v-ff0f39c8><div class="content" data-v-ff0f39c8><div class="outline-marker" data-v-ff0f39c8></div><div class="outline-title" data-v-ff0f39c8>On this page</div><nav aria-labelledby="doc-outline-aria-label" data-v-ff0f39c8><span class="visually-hidden" id="doc-outline-aria-label" data-v-ff0f39c8> Table of Contents for current page </span><ul class="root" data-v-ff0f39c8 data-v-d0ee3533><!--[--><!--]--></ul></nav></div></div><!--[--><!--]--><div class="spacer" data-v-3f215769></div><!--[--><!--]--><!----><!--[--><!--]--><!--[--><!--]--></div></div></div></div><div class="content" data-v-6b87e69f><div class="content-container" data-v-6b87e69f><!--[--><!--]--><!----><main class="main" data-v-6b87e69f><div style="position:relative;" class="vp-doc _notebook_%E4%B8%89%E9%AB%98_%E9%AB%98%E5%B9%B6%E5%8F%91" data-v-6b87e69f><div><p><a href="https://mp.weixin.qq.com/s?__biz=MzU1Nzg4NjgyMw==&amp;mid=2247484154&amp;idx=1&amp;sn=f765d8abd2294e105f5d10eec0571c4d&amp;chksm=fc2fbef2cb5837e44858584275c0533e39cf986fa2e6d60bdc0af60a94a0779cf95fa7065ef5&amp;mpshare=1&amp;scene=23&amp;srcid=0810fWHPgCJj6ySIW0YX4jMA&amp;sharer_sharetime=1660096847759&amp;sharer_shareid=29b8a04db1dbd975e3bf4e9f47e7ac67#rd" target="_blank" rel="noreferrer">淘宝双11，亿级流量高并发是怎么抗住的？看完这篇你就明白了! (qq.com)</a></p><p><a href="https://mp.weixin.qq.com/s?__biz=MzI3ODcxMzQzMw==&amp;mid=2247548673&amp;idx=2&amp;sn=acf0b7fc722550b47ef13e90020552c4&amp;chksm=eb50ba37dc273321dabe5a88cf0cf40089cbb58cd7b67e51abeca20a237b497fdf3f7bea0047&amp;mpshare=1&amp;scene=23&amp;srcid=1207PRLB4Vr49DKaPzTJpHHk&amp;sharer_sharetime=1670345544072&amp;sharer_shareid=29b8a04db1dbd975e3bf4e9f47e7ac67#rd" target="_blank" rel="noreferrer">面试官：亿级流量架构分布式事务如何实现？我懵了。。</a></p><h1 id="高并发-你真的理解透彻了吗" tabindex="-1">高并发，你真的理解透彻了吗 <a class="header-anchor" href="#高并发-你真的理解透彻了吗" aria-label="Permalink to &quot;高并发，你真的理解透彻了吗&quot;">​</a></h1><p>高并发，几乎是每个程序员都想拥有的经验。原因很简单：随着流量变大，会遇到各种各样的技术问题，比如接口响应超时、CPU load升高、GC频繁、死锁、大数据量存储等等，这些问题能推动我们在技术深度上不断精进。</p><p>在过往的面试中，如果候选人做过高并发的项目，我通常会让对方谈谈对于高并发的理解，但是能系统性地回答好此问题的人并不多，大概分成这样几类：</p><p><strong>1、对数据化的指标没有概念</strong>：不清楚选择什么样的指标来衡量高并发系统？分不清并发量和QPS，甚至不知道自己系统的总用户量、活跃用户量，平峰和高峰时的QPS和TPS等关键数据。</p><p>**2、设计了一些方案，但是细节掌握不透彻：**讲不出该方案要关注的技术点和可能带来的副作用。比如读性能有瓶颈会引入缓存，但是忽视了缓存命中率、热点key、数据一致性等问题。</p><p>**3、理解片面，把高并发设计等同于性能优化：**大谈并发编程、多级缓存、异步化、水平扩容，却忽视高可用设计、服务治理和运维保障。</p><p>**4、掌握大方案，却忽视最基本的东西：**能讲清楚垂直分层、水平分区、缓存等大思路，却没意识去分析数据结构是否合理，算法是否高效，没想过从最根本的IO和计算两个维度去做细节优化。</p><p>这篇文章，我想结合自己的高并发项目经验，系统性地总结下高并发需要掌握的知识和实践思路，希望对你有所帮助。内容分成以下3个部分：</p><ul><li><p>如何理解高并发？</p></li><li><p>高并发系统设计的目标是什么？</p></li><li><p>高并发的实践方案有哪些？</p></li></ul><h2 id="如何理解高并发" tabindex="-1">如何理解高并发？ <a class="header-anchor" href="#如何理解高并发" aria-label="Permalink to &quot;如何理解高并发？&quot;">​</a></h2><p>高并发意味着大流量，需要运用技术手段抵抗流量的冲击，这些手段好比操作流量，能让流量更平稳地被系统所处理，带给用户更好的体验。</p><p>我们常见的高并发场景有：淘宝的双11、春运时的抢票、微博大V的热点新闻等。除了这些典型事情，每秒几十万请求的秒杀系统、每天千万级的订单系统、每天亿级日活的信息流系统等，都可以归为高并发。</p><p>很显然，上面谈到的高并发场景，并发量各不相同，<strong>那到底多大并发才算高并发呢？</strong></p><p>1、不能只看数字，要看具体的业务场景。不能说10W QPS的秒杀是高并发，而1W QPS的信息流就不是高并发。信息流场景涉及复杂的推荐模型和各种人工策略，它的业务逻辑可能比秒杀场景复杂10倍不止。因此，不在同一个维度，没有任何比较意义。</p><p>2、业务都是从0到1做起来的，并发量和QPS只是参考指标，最重要的是：在业务量逐渐变成原来的10倍、100倍的过程中，你是否用到了高并发的处理方法去演进你的系统，从架构设计、编码实现、甚至产品方案等维度去预防和解决高并发引起的问题？而不是一味的升级硬件、加机器做水平扩展。</p><p>此外，各个高并发场景的业务特点完全不同：有读多写少的信息流场景、有读多写多的交易场景，<strong>那是否有通用的技术方案解决不同场景的高并发问题呢？</strong></p><p>我觉得大的思路可以借鉴，别人的方案也可以参考，但是真正落地过程中，细节上还会有无数的坑。另外，由于软硬件环境、技术栈、以及产品逻辑都没法做到完全一致，这些都会导致同样的业务场景，就算用相同的技术方案也会面临不同的问题，这些坑还得一个个趟。</p><h2 id="高并发系统设计的目标是什么" tabindex="-1">高并发系统设计的目标是什么 <a class="header-anchor" href="#高并发系统设计的目标是什么" aria-label="Permalink to &quot;高并发系统设计的目标是什么&quot;">​</a></h2><p>先搞清楚高并发系统设计的目标，在此基础上再讨论设计方案和实践经验才有意义和针对性。</p><p>高并发绝不意味着只追求高性能，这是很多人片面的理解。从宏观角度看，高并发系统设计的目标有三个：高性能、高可用，以及高可扩展。</p><p>1、高性能：性能体现了系统的并行处理能力，在有限的硬件投入下，提高性能意味着节省成本。同时，性能也反映了用户体验，响应时间分别是100毫秒和1秒，给用户的感受是完全不同的。</p><p>2、高可用：表示系统可以正常服务的时间。一个全年不停机、无故障；另一个隔三差五出线上事故、宕机，用户肯定选择前者。另外，如果系统只能做到90%可用，也会大大拖累业务。</p><p>3、高扩展：表示系统的扩展能力，流量高峰时能否在短时间内完成扩容，更平稳地承接峰值流量，比如双11活动、明星离婚等热点事件。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207121116048.png" alt="image-20220712111630980" style="zoom:50%;"><p>这3个目标是需要通盘考虑的，因为它们互相关联、甚至也会相互影响。</p><p>比如说：考虑系统的扩展能力，你会将服务设计成无状态的，这种集群设计保证了高扩展性，其实也间接提升了系统的性能和可用性。</p><p>再比如说：为了保证可用性，通常会对服务接口进行超时设置，以防大量线程阻塞在慢请求上造成系统雪崩，那超时时间设置成多少合理呢？一般，我们会参考依赖服务的性能表现进行设置。</p><h2 id="重要指标" tabindex="-1">重要指标 <a class="header-anchor" href="#重要指标" aria-label="Permalink to &quot;重要指标&quot;">​</a></h2><p>再从微观角度来看，高性能、高可用和高扩展又有哪些具体的指标来衡量？为什么会选择这些指标呢？</p><h3 id="❇-性能指标" tabindex="-1">❇ 性能指标 <a class="header-anchor" href="#❇-性能指标" aria-label="Permalink to &quot;❇ 性能指标&quot;">​</a></h3><p>通过性能指标可以度量目前存在的性能问题，同时作为性能优化的评估依据。一般来说，会采用一段时间内的接口响应时间作为指标。</p><p>1、平均响应时间：最常用，但是缺陷很明显，对于慢请求不敏感。比如1万次请求，其中9900次是1ms，100次是100ms，则平均响应时间为1.99ms，虽然平均耗时仅增加了0.99ms，但是1%请求的响应时间已经增加了100倍。</p><p>2、TP90、TP99等分位值：将响应时间按照从小到大排序，TP90表示排在第90分位的响应时间， 分位值越大，对慢请求越敏感。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207121117036.png" alt="image-20220712111702948" style="zoom:67%;"><p>3、吞吐量：和响应时间呈反比，比如响应时间是1ms，则吞吐量为每秒1000次。</p><p>通常，设定性能目标时会兼顾吞吐量和响应时间，比如这样表述：在每秒1万次请求下，AVG控制在50ms以下，TP99控制在100ms以下。对于高并发系统，AVG和TP分位值必须同时要考虑。</p><p>另外，从用户体验角度来看，200毫秒被认为是第一个分界点，用户感觉不到延迟，1秒是第二个分界点，用户能感受到延迟，但是可以接受。</p><p>因此，对于一个健康的高并发系统，TP99应该控制在200毫秒以内，TP999或者TP9999应该控制在1秒以内。</p><h3 id="❇-可用性指标" tabindex="-1">❇ 可用性指标 <a class="header-anchor" href="#❇-可用性指标" aria-label="Permalink to &quot;❇ 可用性指标&quot;">​</a></h3><p>高可用性是指系统具有较高的无故障运行能力，可用性 = 正常运行时间 / 系统总运行时间，一般使用几个9来描述系统的可用性。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207121117081.png" alt="image-20220712111717009" style="zoom:67%;"><p>对于高并发系统来说，最基本的要求是：保证3个9或者4个9。原因很简单，如果你只能做到2个9，意味着有1%的故障时间，像一些大公司每年动辄千亿以上的GMV或者收入，1%就是10亿级别的业务影响。</p><h3 id="❇-可扩展性指标" tabindex="-1">❇ 可扩展性指标 <a class="header-anchor" href="#❇-可扩展性指标" aria-label="Permalink to &quot;❇ 可扩展性指标&quot;">​</a></h3><p>面对突发流量，不可能临时改造架构，最快的方式就是增加机器来线性提高系统的处理能力。</p><p>对于业务集群或者基础组件来说，扩展性 = 性能提升比例 / 机器增加比例，理想的扩展能力是：资源增加几倍，性能提升几倍。通常来说，扩展能力要维持在70%以上。</p><p>但是从高并发系统的整体架构角度来看，扩展的目标不仅仅是把服务设计成无状态就行了，因为当流量增加10倍，业务服务可以快速扩容10倍，但是数据库可能就成为了新的瓶颈。</p><p>像MySQL这种有状态的存储服务通常是扩展的技术难点，如果架构上没提前做好规划（垂直和水平拆分），就会涉及到大量数据的迁移。</p><p>因此，高扩展性需要考虑：服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等，当并发达到某一个量级后，上述每个因素都可能成为扩展的瓶颈点。</p><h2 id="高并发的实践方案有哪些" tabindex="-1">高并发的实践方案有哪些？ <a class="header-anchor" href="#高并发的实践方案有哪些" aria-label="Permalink to &quot;高并发的实践方案有哪些？&quot;">​</a></h2><p>了解了高并发设计的3大目标后，再系统性总结下高并发的设计方案，会从以下两部分展开：先总结下通用的设计方法，然后再围绕高性能、高可用、高扩展分别给出具体的实践方案。</p><p>通用的设计方法主要是从「纵向」和「横向」两个维度出发，俗称高并发处理的两板斧：纵向扩展和横向扩展。</p><h3 id="❇-纵向扩展-scale-up" tabindex="-1">❇ 纵向扩展（scale-up） <a class="header-anchor" href="#❇-纵向扩展-scale-up" aria-label="Permalink to &quot;❇ 纵向扩展（scale-up）&quot;">​</a></h3><p>它的目标是提升单机的处理能力，方案又包括：</p><p>1、提升单机的硬件性能：通过增加内存、CPU核数、存储容量、或者将磁盘升级成SSD等堆硬件的方式来提升。</p><p>2、提升单机的软件性能：使用缓存减少IO次数，使用并发或者异步的方式增加吞吐量。</p><h3 id="❇-横向扩展-scale-out" tabindex="-1">❇ 横向扩展（scale-out） <a class="header-anchor" href="#❇-横向扩展-scale-out" aria-label="Permalink to &quot;❇ 横向扩展（scale-out）&quot;">​</a></h3><p>因为单机性能总会存在极限，所以最终还需要引入横向扩展，通过集群部署以进一步提高并发处理能力，又包括以下2个方向：</p><p>1、做好分层架构：这是横向扩展的提前，因为高并发系统往往业务复杂，通过分层处理可以简化复杂问题，更容易做到横向扩展。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207121119830.png" alt="image-20220712111903748" style="zoom:50%;"><p>上面这种图是互联网最常见的分层架构，当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN，反向代理层可以是LVS+Nginx，Web层可以是统一的API网关，业务服务层可进一步按垂直业务做微服务化，存储层可以是各种异构数据库。</p><p>2、各层进行水平扩展：无状态水平扩容，有状态做分片路由。业务集群通常能设计成无状态的，而数据库和缓存往往是有状态的，因此需要设计分区键做好存储分片，当然也可以通过主从同步、读写分离的方案提升读性能。</p><p>下面再结合我的个人经验，针对高性能、高可用、高扩展3个方面，总结下可落地的实践方案。</p><h2 id="❇-高性能的实践方案" tabindex="-1">❇ 高性能的实践方案 <a class="header-anchor" href="#❇-高性能的实践方案" aria-label="Permalink to &quot;❇ 高性能的实践方案&quot;">​</a></h2><p>1、集群部署，通过负载均衡减轻单机压力。</p><p>2、多级缓存，包括静态数据使用CDN、本地缓存、分布式缓存等，以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。</p><p>3、分库分表和索引优化，以及借助搜索引擎解决复杂查询问题。</p><p>4、考虑NoSQL数据库的使用，比如HBase、TiDB等，但是团队必须熟悉这些组件，且有较强的运维能力。</p><p>5、异步化，将次要流程通过多线程、MQ、甚至延时任务进行异步处理。</p><p>6、限流，需要先考虑业务是否允许限流（比如秒杀场景是允许的），包括前端限流、Nginx接入层的限流、服务端的限流。</p><p>7、对流量进行削峰填谷，通过MQ承接流量。</p><p>8、并发处理，通过多线程将串行逻辑并行化。</p><p>9、预计算，比如抢红包场景，可以提前计算好红包金额缓存起来，发红包时直接使用即可。</p><p>10、缓存预热，通过异步任务提前预热数据到本地缓存或者分布式缓存中。</p><p>11、减少IO次数，比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。</p><p>12、减少IO时的数据包大小，包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。</p><p>13、程序逻辑优化，比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化，或者采用更高效的算法。</p><p>14、各种池化技术的使用和池大小的设置，包括HTTP请求池、线程池（考虑CPU密集型还是IO密集型设置核心参数）、数据库和Redis连接池等。</p><p>15、JVM优化，包括新生代和老年代的大小、GC算法的选择等，尽可能减少GC频率和耗时。</p><p>16、锁选择，读多写少的场景用乐观锁，或者考虑通过分段锁的方式减少锁冲突。</p><p>上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点，需要有配套的监控系统实时了解当前的性能表现，并支撑你进行性能瓶颈分析，然后再遵循二八原则，抓主要矛盾进行优化。</p><h2 id="❇-高可用的实践方案" tabindex="-1">❇ 高可用的实践方案 <a class="header-anchor" href="#❇-高可用的实践方案" aria-label="Permalink to &quot;❇ 高可用的实践方案&quot;">​</a></h2><p>1、对等节点的故障转移，Nginx和服务治理框架均支持一个节点失败后访问另一个节点。</p><p>2、非对等节点的故障转移，通过心跳检测并实施主备切换（比如redis的哨兵模式或者集群模式、MySQL的主从切换等）。</p><p>3、接口层面的超时设置、重试策略和幂等设计。</p><p>4、降级处理：保证核心服务，牺牲非核心服务，必要时进行熔断；或者核心链路出问题时，有备选链路。</p><p>5、限流处理：对超过系统处理能力的请求直接拒绝或者返回错误码。</p><p>6、MQ场景的消息可靠性保证，包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。</p><p>7、灰度发布，能支持按机器维度进行小流量部署，观察系统日志和业务指标，等运行平稳后再推全量。</p><p>8、监控报警：全方位的监控体系，包括最基础的CPU、内存、磁盘、网络的监控，以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。</p><p>9、灾备演练：类似当前的“混沌工程”，对系统进行一些破坏性手段，观察局部故障是否会引起可用性问题。</p><p>高可用的方案主要从冗余、取舍、系统运维3个方向考虑，同时需要有配套的值班机制和故障处理流程，当出现线上问题时，可及时跟进处理。</p><h2 id="❇-高扩展的实践方案" tabindex="-1">❇ 高扩展的实践方案 <a class="header-anchor" href="#❇-高扩展的实践方案" aria-label="Permalink to &quot;❇ 高扩展的实践方案&quot;">​</a></h2><p>1、合理的分层架构：比如上面谈到的互联网最常见的分层架构，另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层（但是需要评估性能，会存在网络多一跳的情况）。</p><p>2、存储层的拆分：按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分（分库分表）。</p><p>3、业务层的拆分：最常见的是按照业务维度拆（比如电商场景的商品服务、订单服务等），也可以按照核心接口和非核心接口拆，还可以按照请求源拆（比如To C和To B，APP和H5）。</p><p>高并发确实是一个复杂且系统性的问题，由于篇幅有限，诸如分布式Trace、全链路压测、柔性事务都是要考虑的技术点。另外，如果业务场景不同，高并发的落地方案也会存在差异，但是总体的设计思路和可借鉴的方案基本类似。</p><p>高并发设计同样要秉承架构设计的3个原则：简单、合适和演进。“过早的优化是万恶之源”，不能脱离业务的实际情况，更不要过度设计，合适的方案就是最完美的。</p><h1 id="高并发下秒杀商品" tabindex="-1">高并发下秒杀商品 <a class="header-anchor" href="#高并发下秒杀商品" aria-label="Permalink to &quot;高并发下秒杀商品&quot;">​</a></h1><p><a href="https://mp.weixin.qq.com/s?__biz=MzU1NTkwODE4Mw==&amp;mid=2247498716&amp;idx=1&amp;sn=18e574f2c96dba3638b6c188e6a1026a&amp;chksm=fbcf96e4ccb81ff29bb372af7ddbed96b4af6fd641c423cc2b77cf0151ba601106409c1fb419&amp;mpshare=1&amp;scene=23&amp;srcid=0702UGt7rO4nlL9sNTkscLXr&amp;sharer_sharetime=1656775000153&amp;sharer_shareid=29b8a04db1dbd975e3bf4e9f47e7ac67#rd" target="_blank" rel="noreferrer">高并发下秒杀商品，必须知道的9个细节 (qq.com)</a></p><p>高并发下如何设计秒杀系统？这是一个高频面试题。这个问题看似简单，但是里面的水很深，它考查的是高并发场景下，从前端到后端多方面的知识。</p><p>秒杀一般出现在商城的<code>促销活动</code>中，指定了一定数量（比如：10个）的商品（比如：手机），以极低的价格（比如：0.1元），让大量用户参与活动，但只有极少数用户能够购买成功。这类活动商家绝大部分是不赚钱的，说白了是找个噱头宣传自己。</p><p>虽说秒杀只是一个促销活动，但对技术要求不低。下面给大家总结一下设计秒杀系统需要注意的9个细节。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060024059.png" alt="image-20220706002414973" style="zoom:33%;"><h2 id="_1-瞬时高并发" tabindex="-1">1 瞬时高并发 <a class="header-anchor" href="#_1-瞬时高并发" aria-label="Permalink to &quot;1 瞬时高并发&quot;">​</a></h2><p>一般在<code>秒杀时间点</code>（比如：12点）前几分钟，用户并发量才真正突增，达到秒杀时间点时，并发量会达到顶峰。</p><p>但由于这类活动是大量用户抢少量商品的场景，必定会出现<code>狼多肉少</code>的情况，所以其实绝大部分用户秒杀会失败，只有极少部分用户能够成功。</p><p>正常情况下，大部分用户会收到商品已经抢完的提醒，收到该提醒后，他们大概率不会在那个活动页面停留了，如此一来，用户并发量又会急剧下降。所以这个峰值持续的时间其实是非常短的，这样就会出现瞬时高并发的情况，下面用一张图直观的感受一下流量的变化：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060024970.png" alt="image-20220706002431924" style="zoom:50%;"><p>像这种瞬时高并发的场景，传统的系统很难应对，我们需要设计一套全新的系统。可以从以下几个方面入手：</p><ol><li>页面静态化</li><li>CDN加速</li><li>缓存</li><li>mq异步处理</li><li>限流</li><li>分布式锁</li></ol><h2 id="_2-页面静态化" tabindex="-1">2. 页面静态化 <a class="header-anchor" href="#_2-页面静态化" aria-label="Permalink to &quot;2. 页面静态化&quot;">​</a></h2><p>活动页面是用户流量的第一入口，所以是并发量最大的地方。</p><p>如果这些流量都能直接访问服务端，恐怕服务端会因为承受不住这么大的压力，而直接挂掉。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060024730.png" alt="image-20220706002456631" style="zoom:67%;"><p>活动页面绝大多数内容是固定的，比如：商品名称、商品描述、图片等。为了减少不必要的服务端请求，通常情况下，会对活动页面做<code>静态化</code>处理。用户浏览商品等常规操作，并不会请求到服务端。只有到了秒杀时间点，并且用户主动点了秒杀按钮才允许访问服务端。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060025611.png" alt="image-20220706002508517" style="zoom:50%;"><p>这样能过滤大部分无效请求。</p><p>但只做页面静态化还不够，因为用户分布在全国各地，有些人在北京，有些人在成都，有些人在深圳，地域相差很远，网速各不相同。</p><p>如何才能让用户最快访问到活动页面呢？</p><p>这就需要使用CDN，它的全称是Content Delivery Network，即内容分发网络。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060025650.png" alt="image-20220706002528524" style="zoom:50%;"><p>使用户就近获取所需内容，降低网络拥塞，提高用户访问响应速度和命中率。</p><h2 id="_3-秒杀按钮" tabindex="-1">3 秒杀按钮 <a class="header-anchor" href="#_3-秒杀按钮" aria-label="Permalink to &quot;3 秒杀按钮&quot;">​</a></h2><p>大部分用户怕错过<code>秒杀时间点</code>，一般会提前进入活动页面。此时看到的<code>秒杀按钮</code>是置灰，不可点击的。只有到了秒杀时间点那一时刻，秒杀按钮才会自动点亮，变成可点击的。</p><p>但此时很多用户已经迫不及待了，通过不停刷新页面，争取在第一时间看到秒杀按钮的点亮。</p><p>从前面得知，该活动页面是静态的。那么我们在静态页面中如何控制秒杀按钮，只在秒杀时间点时才点亮呢？</p><p>没错，使用js文件控制。</p><p>为了性能考虑，一般会将css、js和图片等静态资源文件提前缓存到CDN上，让用户能够就近访问秒杀页面。</p><p>看到这里，有些聪明的小伙伴，可能会问：CDN上的js文件是如何更新的？</p><p>秒杀开始之前，js标志为false，还有另外一个随机参数。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060025835.png" alt="image-20220706002559721" style="zoom:50%;"><p>当秒杀开始的时候系统会生成一个新的js文件，此时标志为true，并且随机参数生成一个新值，然后同步给CDN。由于有了这个随机参数，CDN不会缓存数据，每次都能从CDN中获取最新的js代码。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060026446.png" alt="image-20220706002616329" style="zoom:50%;"><p>此外，前端还可以加一个定时器，控制比如：10秒之内，只允许发起一次请求。如果用户点击了一次秒杀按钮，则在10秒之内置灰，不允许再次点击，等到过了时间限制，又允许重新点击该按钮。</p><h2 id="_4-读多写少" tabindex="-1">4 读多写少 <a class="header-anchor" href="#_4-读多写少" aria-label="Permalink to &quot;4 读多写少&quot;">​</a></h2><p>在秒杀的过程中，系统一般会先查一下库存是否足够，如果足够才允许下单，写数据库。如果不够，则直接返回该商品已经抢完。</p><p>由于大量用户抢少量商品，只有极少部分用户能够抢成功，所以绝大部分用户在秒杀时，库存其实是不足的，系统会直接返回该商品已经抢完。</p><p>这是非常典型的：<code>读多写少</code> 的场景。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060026973.png" alt="image-20220706002639878" style="zoom:50%;"><p>如果有数十万的请求过来，同时通过数据库查缓存是否足够，此时数据库可能会挂掉。因为数据库的连接资源非常有限，比如：mysql，无法同时支持这么多的连接。</p><p>而应该改用缓存，比如：redis。</p><p>即便用了redis，也需要部署多个节点。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060028766.png" alt="image-20220706002721411" style="zoom:50%;"><h2 id="_5-缓存问题" tabindex="-1">5 缓存问题 <a class="header-anchor" href="#_5-缓存问题" aria-label="Permalink to &quot;5 缓存问题&quot;">​</a></h2><p>通常情况下，我们需要在redis中保存商品信息，里面包含：商品id、商品名称、规格属性、库存等信息，同时数据库中也要有相关信息，毕竟缓存并不完全可靠。</p><p>用户在点击秒杀按钮，请求秒杀接口的过程中，需要传入的商品id参数，然后服务端需要校验该商品是否合法。</p><p>大致流程如下图所示：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060021936.png" alt="image-20220706002130870" style="zoom:50%;"><p>根据商品id，先从缓存中查询商品，如果商品存在，则参与秒杀。如果不存在，则需要从数据库中查询商品，如果存在，则将商品信息放入缓存，然后参与秒杀。如果商品不存在，则直接提示失败。</p><p>这个过程表面上看起来是OK的，但是如果深入分析一下会发现一些问题。</p><h3 id="_5-1-缓存击穿" tabindex="-1">5.1 缓存击穿 <a class="header-anchor" href="#_5-1-缓存击穿" aria-label="Permalink to &quot;5.1 缓存击穿&quot;">​</a></h3><p>比如商品A第一次秒杀时，缓存中是没有数据的，但数据库中有。虽说上面有如果从数据库中查到数据，则放入缓存的逻辑。</p><p>然而，在高并发下，同一时刻会有大量的请求，都在秒杀同一件商品，这些请求同时去查缓存中没有数据，然后又同时访问数据库。结果悲剧了，数据库可能扛不住压力，直接挂掉。</p><p>如何解决这个问题呢？</p><p>这就需要加锁，最好使用分布式锁。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060021157.png" alt="image-20220706002155099" style="zoom:67%;"><p><img src="" alt="图片">当然，针对这种情况，最好在项目启动之前，先把缓存进行<code>预热</code>。即事先把所有的商品，同步到缓存中，这样商品基本都能直接从缓存中获取到，就不会出现缓存击穿的问题了。</p><p>是不是上面加锁这一步可以不需要了？</p><p>表面上看起来，确实可以不需要。但如果缓存中设置的过期时间不对，缓存提前过期了，或者缓存被不小心删除了，如果不加速同样可能出现缓存击穿。</p><p>其实这里加锁，相当于买了一份保险。</p><h3 id="_5-2-缓存穿透" tabindex="-1">5.2 缓存穿透 <a class="header-anchor" href="#_5-2-缓存穿透" aria-label="Permalink to &quot;5.2 缓存穿透&quot;">​</a></h3><p>如果有大量的请求传入的商品id，在缓存中和数据库中都不存在，这些请求不就每次都会穿透过缓存，而直接访问数据库了。</p><p>由于前面已经加了锁，所以即使这里的并发量很大，也不会导致数据库直接挂掉。</p><p>但很显然这些请求的处理性能并不好，有没有更好的解决方案？</p><p>这时可以想到<code>布隆过滤器</code>。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060022397.png" alt="image-20220706002236320" style="zoom:50%;"><p>系统根据商品id，先从布隆过滤器中查询该id是否存在，如果存在则允许从缓存中查询数据，如果不存在，则直接返回失败。</p><p>虽说该方案可以解决缓存穿透问题，但是又会引出另外一个问题：布隆过滤器中的数据如何更缓存中的数据保持一致？</p><p>这就要求，如果缓存中数据有更新，则要及时同步到布隆过滤器中。如果数据同步失败了，还需要增加重试机制，而且跨数据源，能保证数据的实时一致性吗？</p><p>显然是不行的。</p><p>所以布隆过滤器绝大部分使用在缓存数据更新很少的场景中。</p><p>如果缓存数据更新非常频繁，又该如何处理呢？</p><p>这时，就需要把不存在的商品id也缓存起来。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060023097.png" alt="image-20220706002306018" style="zoom:50%;"><p><img src="" alt="图片">下次，再有该商品id的请求过来，则也能从缓存中查到数据，只不过该数据比较特殊，表示商品不存在。需要特别注意的是，这种特殊缓存设置的超时时间应该尽量短一点。</p><h2 id="_6-库存问题" tabindex="-1">6 库存问题 <a class="header-anchor" href="#_6-库存问题" aria-label="Permalink to &quot;6 库存问题&quot;">​</a></h2><p>对于库存问题看似简单，实则里面还是有些东西。</p><p>真正的秒杀商品的场景，不是说扣完库存，就完事了，如果用户在一段时间内，还没完成支付，扣减的库存是要加回去的。</p><p>所以，在这里引出了一个<code>预扣库存</code>的概念，预扣库存的主要流程如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060019268.png" alt="image-20220706001939172" style="zoom:33%;"><p>扣减库存中除了上面说到的<code>预扣库存</code>和<code>回退库存</code>之外，还需要特别注意的是库存不足和库存超卖问题。</p><h3 id="_6-1-数据库扣减库存" tabindex="-1">6.1 数据库扣减库存 <a class="header-anchor" href="#_6-1-数据库扣减库存" aria-label="Permalink to &quot;6.1 数据库扣减库存&quot;">​</a></h3><p>使用数据库扣减库存，是最简单的实现方案了，假设扣减库存的sql如下：</p><div class="language-sql"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">update</span><span style="color:#A6ACCD;"> product </span><span style="color:#F78C6C;">set</span><span style="color:#A6ACCD;"> stock</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">stock</span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">where</span><span style="color:#A6ACCD;"> id</span><span style="color:#89DDFF;">=</span><span style="color:#F78C6C;">123</span><span style="color:#A6ACCD;">;</span></span></code></pre></div><p>这种写法对于扣减库存是没有问题的，但如何控制库存不足的情况下，不让用户操作呢？</p><p>这就需要在update之前，先查一下库存是否足够了。</p><p>伪代码如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> mapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getStockById</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">123</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">stock </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> count </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> mapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">updateStock</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">123</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">count </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#82AAFF;">addOrder</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">123</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>大家有没有发现这段代码的问题？</p><p>没错，查询操作和更新操作不是原子性的，会导致在并发的场景下，出现库存超卖的情况。</p><p>有人可能会说，这样好办，加把锁，不就搞定了，比如使用synchronized关键字。</p><p>确实，可以，但是性能不够好。</p><p>还有更优雅的处理方案，即基于数据库的乐观锁，这样会少一次数据库查询，而且能够天然的保证数据操作的原子性。</p><p>只需将上面的sql稍微调整一下：</p><div class="language-sql"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">update</span><span style="color:#A6ACCD;"> product </span><span style="color:#F78C6C;">set</span><span style="color:#A6ACCD;"> stock</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">stock</span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">where</span><span style="color:#A6ACCD;"> id</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">product </span><span style="color:#F78C6C;">and</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#A6ACCD;">;</span></span></code></pre></div><p>在sql最后加上：<code>stock &gt; 0</code>，就能保证不会出现超卖的情况。</p><p>但需要频繁访问数据库，我们都知道数据库连接是非常昂贵的资源。在高并发的场景下，可能会造成系统雪崩。而且，容易出现多个请求，同时竞争行锁的情况，造成相互等待，从而出现死锁的问题。</p><h3 id="_6-2-redis扣减库存" tabindex="-1">6.2 redis扣减库存 <a class="header-anchor" href="#_6-2-redis扣减库存" aria-label="Permalink to &quot;6.2 redis扣减库存&quot;">​</a></h3><p>redis的<code>incr</code>方法是原子性的，可以用该方法扣减库存。伪代码如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> exist </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">exist</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">queryStock</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">stock </span><span style="color:#89DDFF;">&lt;=</span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">  redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">incrby</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">add</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span></code></pre></div><p>代码流程如下：</p><ol><li>先判断该用户有没有秒杀过该商品，如果已经秒杀过，则直接返回-1。</li><li>查询库存，如果库存小于等于0，则直接返回0，表示库存不足。</li><li>如果库存充足，则扣减库存，然后将本次秒杀记录保存起来。然后返回1，表示成功。</li></ol><p>估计很多小伙伴，一开始都会按这样的思路写代码。但如果仔细想想会发现，这段代码有问题。</p><p>有什么问题呢？</p><p>如果在高并发下，有多个请求同时查询库存，当时都大于0。由于查询库存和更新库存非原则操作，则会出现库存为负数的情况，即<code>库存超卖</code>。</p><p>当然有人可能会说，加个<code>synchronized</code>不就解决问题？</p><p>调整后代码如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> exist </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">exist</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#C792EA;">synchronized</span><span style="color:#89DDFF;">(this)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> stock </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">queryStock</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">stock </span><span style="color:#89DDFF;">&lt;=</span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">         </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">       redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">incrby</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">       redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">add</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#89DDFF;">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span></code></pre></div><p>加<code>synchronized</code>确实能解决库存为负数问题，但是这样会导致接口性能急剧下降，每次查询都需要竞争同一把锁，显然不太合理。</p><p>为了解决上面的问题，代码优化如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> exist </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">exist</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">incrby</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">)&lt;</span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">redisClient</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">add</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">userId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">;</span></span></code></pre></div><p>该代码主要流程如下：</p><ol><li>先判断该用户有没有秒杀过该商品，如果已经秒杀过，则直接返回-1。</li><li>扣减库存，判断返回值是否小于0，如果小于0，则直接返回0，表示库存不足。</li><li>如果扣减库存后，返回值大于或等于0，则将本次秒杀记录保存起来。然后返回1，表示成功。</li></ol><p>该方案咋一看，好像没问题。</p><p>但如果在高并发场景中，有多个请求同时扣减库存，大多数请求的incrby操作之后，结果都会小于0。</p><p>虽说，库存出现负数，不会出现<code>超卖的问题</code>。但由于这里是预减库存，如果负数值负的太多的话，后面万一要回退库存时，就会导致库存不准。</p><p>那么，有没有更好的方案呢？</p><h3 id="_6-3-lua脚本扣减库存" tabindex="-1">6.3 lua脚本扣减库存 <a class="header-anchor" href="#_6-3-lua脚本扣减库存" aria-label="Permalink to &quot;6.3 lua脚本扣减库存&quot;">​</a></h3><p>我们都知道lua脚本，是能够保证原子性的，它跟redis一起配合使用，能够完美解决上面的问题。</p><p>lua脚本有段非常经典的代码：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">StringBuilder</span><span style="color:#A6ACCD;"> lua </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">new</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">StringBuilder</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">if (redis.call(&#39;exists&#39;, KEYS[1]) == 1) then</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    local stock = tonumber(redis.call(&#39;get&#39;, KEYS[1]));</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    if (stock == -1) then</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">        return 1;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    end;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    if (stock &gt; 0) then</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">        redis.call(&#39;incrby&#39;, KEYS[1], -1);</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">        return stock;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    end;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">    return 0;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">end;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  lua</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">append</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">return -1;</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span></code></pre></div><p>该代码的主要流程如下：</p><ol><li>先判断商品id是否存在，如果不存在则直接返回。</li><li>获取该商品id的库存，判断库存如果是-1，则直接返回，表示不限制库存。</li><li>如果库存大于0，则扣减库存。</li><li>如果库存等于0，是直接返回，表示库存不足。</li></ol><h2 id="_7-分布式锁" tabindex="-1">7 分布式锁 <a class="header-anchor" href="#_7-分布式锁" aria-label="Permalink to &quot;7 分布式锁&quot;">​</a></h2><p>之前我提到过，在秒杀的时候，需要先从缓存中查商品是否存在，如果不存在，则会从数据库中查商品。如果数据库中，则将该商品放入缓存中，然后返回。如果数据库中没有，则直接返回失败。</p><p>大家试想一下，如果在高并发下，有大量的请求都去查一个缓存中不存在的商品，这些请求都会直接打到数据库。数据库由于承受不住压力，而直接挂掉。</p><p>那么如何解决这个问题呢？</p><p>这就需要用redis分布式锁了。</p><h3 id="_7-1-setnx加锁" tabindex="-1">7.1 setNx加锁 <a class="header-anchor" href="#_7-1-setnx加锁" aria-label="Permalink to &quot;7.1 setNx加锁&quot;">​</a></h3><p>使用redis的分布式锁，首先想到的是<code>setNx</code>命令。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">setnx</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> val</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">==</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">   jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">expire</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> timeout</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>用该命令其实可以加锁，但和后面的设置超时时间是分开的，并非原子操作。</p><p>假如加锁成功了，但是设置超时时间失败了，该lockKey就变成永不失效的了。在高并发场景中，该问题会导致非常严重的后果。</p><p>那么，有没有保证原子性的加锁命令呢？</p><h3 id="_7-2-set加锁" tabindex="-1">7.2 set加锁 <a class="header-anchor" href="#_7-2-set加锁" aria-label="Permalink to &quot;7.2 set加锁&quot;">​</a></h3><p>使用redis的set命令，它可以指定多个参数。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> result </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> requestId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">NX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">PX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> expireTime</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">OK</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">result</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span></code></pre></div><p>其中：</p><ul><li>lockKey：锁的标识</li><li>requestId：请求id</li><li>NX：只在键不存在时，才对键进行设置操作。</li><li>PX：设置键的过期时间为 millisecond 毫秒。</li><li>expireTime：过期时间</li></ul><p>由于该命令只有一步，所以它是原子操作。</p><h3 id="_7-3-释放锁" tabindex="-1">7.3 释放锁 <a class="header-anchor" href="#_7-3-释放锁" aria-label="Permalink to &quot;7.3 释放锁&quot;">​</a></h3><p>接下来，有些朋友可能会问：在加锁时，既然已经有了lockKey锁标识，为什么要需要记录requestId呢？</p><p>答：requestId是在释放锁的时候用的。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">get</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">).</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">requestId</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">del</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span></code></pre></div><p>在释放锁的时候，只能释放自己加的锁，不允许释放别人加的锁。</p><p>这里为什么要用requestId，用userId不行吗？</p><p>答：如果用userId的话，假设本次请求流程走完了，准备删除锁。此时，巧合锁到了过期时间失效了。而另外一个请求，巧合使用的相同userId加锁，会成功。而本次请求删除锁的时候，删除的其实是别人的锁了。</p><p>当然使用lua脚本也能避免该问题：</p><div class="language-lua"><button title="Copy Code" class="copy"></button><span class="lang">lua</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> redis.</span><span style="color:#82AAFF;">call</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">get</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">, KEYS[</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;">]) </span><span style="color:#89DDFF;">==</span><span style="color:#A6ACCD;"> ARGV[</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;">] </span><span style="color:#89DDFF;font-style:italic;">then</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> redis.</span><span style="color:#82AAFF;">call</span><span style="color:#A6ACCD;">(</span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">del</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">, KEYS[</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;">]) </span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">end</span></span></code></pre></div><p>它能保证查询锁是否存在和删除锁是原子操作。</p><h3 id="_7-4-自旋锁" tabindex="-1">7.4 自旋锁 <a class="header-anchor" href="#_7-4-自旋锁" aria-label="Permalink to &quot;7.4 自旋锁&quot;">​</a></h3><p>上面的加锁方法看起来好像没有问题，但如果你仔细想想，如果有1万的请求同时去竞争那把锁，可能只有一个请求是成功的，其余的9999个请求都会失败。</p><p>在秒杀场景下，会有什么问题？</p><p>答：每1万个请求，有1个成功。再1万个请求，有1个成功。如此下去，直到库存不足。这就变成均匀分布的秒杀了，跟我们想象中的不一样。</p><p>如何解决这个问题呢？</p><p>答：使用自旋锁。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">Long</span><span style="color:#A6ACCD;"> start </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> System</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">currentTimeMillis</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">while</span><span style="color:#89DDFF;">(true)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> result </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> requestId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">NX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">PX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> expireTime</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">     </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">OK</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">result</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#A6ACCD;">     </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">     </span></span>
<span class="line"><span style="color:#A6ACCD;">     </span><span style="color:#C792EA;">long</span><span style="color:#A6ACCD;"> time </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> System</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">currentTimeMillis</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;"> start</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">time</span><span style="color:#89DDFF;">&gt;=</span><span style="color:#A6ACCD;">timeout</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">          </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">          Thread</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">sleep</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">50</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">InterruptedException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">          e</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">printStackTrace</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">finally</span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#82AAFF;">unlock</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">requestId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;">  </span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span></code></pre></div><p>在规定的时间，比如500毫秒内，自旋不断尝试加锁，如果成功则直接返回。如果失败，则休眠50毫秒，再发起新一轮的尝试。如果到了超时时间，还未加锁成功，则直接返回失败。</p><h3 id="_7-5-redisson" tabindex="-1">7.5 redisson <a class="header-anchor" href="#_7-5-redisson" aria-label="Permalink to &quot;7.5 redisson&quot;">​</a></h3><p>除了上面的问题之外，使用redis分布式锁，还有锁竞争问题、续期问题、锁重入问题、多个redis实例加锁问题等。</p><p>这些问题使用redisson可以解决，由于篇幅的原因，在这里先保留一点悬念，有疑问的私聊给我。后面会出一个专题介绍分布式锁，敬请期待。</p><h2 id="_8-mq异步处理" tabindex="-1">8 mq异步处理 <a class="header-anchor" href="#_8-mq异步处理" aria-label="Permalink to &quot;8 mq异步处理&quot;">​</a></h2><p>我们都知道在真实的秒杀场景中，有三个核心流程：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060017680.png" alt="image-20220706001708633" style="zoom:67%;"><p>而这三个核心流程中，真正并发量大的是秒杀功能，下单和支付功能实际并发量很小。所以，我们在设计秒杀系统时，有必要把下单和支付功能从秒杀的主流程中拆分出来，特别是下单功能要做成mq异步处理的。而支付功能，比如支付宝支付，是业务场景本身保证的异步。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060017192.png" alt="image-20220706001733137" style="zoom:50%;"><p>于是，秒杀后下单的流程变成如下：<img src="" alt="图片">如果使用mq，需要关注以下几个问题：</p><h3 id="_8-1-消息丢失问题" tabindex="-1">8.1 消息丢失问题 <a class="header-anchor" href="#_8-1-消息丢失问题" aria-label="Permalink to &quot;8.1 消息丢失问题&quot;">​</a></h3><p>秒杀成功了，往mq发送下单消息的时候，有可能会失败。原因有很多，比如：网络问题、broker挂了、mq服务端磁盘问题等。这些情况，都可能会造成消息丢失。</p><p>那么，如何防止消息丢失呢？</p><p>答：加一张消息发送表。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060016867.png" alt="image-20220706001602798" style="zoom:50%;"><p><img src="" alt="图片">在生产者发送mq消息之前，先把该条消息写入消息发送表，初始状态是待处理，然后再发送mq消息。消费者消费消息时，处理完业务逻辑之后，再回调生产者的一个接口，修改消息状态为已处理。</p><p>如果生产者把消息写入消息发送表之后，再发送mq消息到mq服务端的过程中失败了，造成了消息丢失。</p><p>这时候，要如何处理呢？</p><p>答：使用job，增加重试机制。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207052333147.png" alt="image-20220705233321089" style="zoom:60%;"><p>用job每隔一段时间去查询消息发送表中状态为待处理的数据，然后重新发送mq消息。</p><h3 id="_8-2-重复消费问题" tabindex="-1">8.2 重复消费问题 <a class="header-anchor" href="#_8-2-重复消费问题" aria-label="Permalink to &quot;8.2 重复消费问题&quot;">​</a></h3><p>本来消费者消费消息时，在ack应答的时候，如果网络超时，本身就可能会消费重复的消息。但由于消息发送者增加了重试机制，会导致消费者重复消息的概率增大。</p><p>那么，如何解决重复消息问题呢？</p><p>答：加一张消息处理表。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207052333710.png" alt="image-20220705233338622" style="zoom:50%;"><p>消费者读到消息之后，先判断一下消息处理表，是否存在该消息，如果存在，表示是重复消费，则直接返回。如果不存在，则进行下单操作，接着将该消息写入消息处理表中，再返回。</p><p>有个比较关键的点是：下单和写消息处理表，要放在同一个事务中，保证原子操作。</p><h3 id="_8-3-垃圾消息问题" tabindex="-1">8.3 垃圾消息问题 <a class="header-anchor" href="#_8-3-垃圾消息问题" aria-label="Permalink to &quot;8.3 垃圾消息问题&quot;">​</a></h3><p>这套方案表面上看起来没有问题，但如果出现了消息消费失败的情况。比如：由于某些原因，消息消费者下单一直失败，一直不能回调状态变更接口，这样job会不停的重试发消息。最后，会产生大量的垃圾消息。</p><p>那么，如何解决这个问题呢？</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060015531.png" alt="image-20220706001511484" style="zoom:67%;"><p>每次在job重试时，需要先判断一下消息发送表中该消息的发送次数是否达到最大限制，如果达到了，则直接返回。如果没有达到，则将次数加1，然后发送消息。</p><p>这样如果出现异常，只会产生少量的垃圾消息，不会影响到正常的业务。</p><h3 id="_8-4-延迟消费问题" tabindex="-1">8.4 延迟消费问题 <a class="header-anchor" href="#_8-4-延迟消费问题" aria-label="Permalink to &quot;8.4 延迟消费问题&quot;">​</a></h3><p>通常情况下，如果用户秒杀成功了，下单之后，在15分钟之内还未完成支付的话，该订单会被自动取消，回退库存。</p><p>那么，在15分钟内未完成支付，订单被自动取消的功能，要如何实现呢？</p><p>我们首先想到的可能是job，因为它比较简单。</p><p>但job有个问题，需要每隔一段时间处理一次，实时性不太好。</p><p>还有更好的方案？</p><p>答：使用延迟队列。</p><p>我们都知道rocketmq，自带了延迟队列的功能。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060014793.png" alt="image-20220706001405738" style="zoom:50%;"><p><img src="" alt="图片">下单时消息生产者会先生成订单，此时状态为待支付，然后会向延迟队列中发一条消息。达到了延迟时间，消息消费者读取消息之后，会查询该订单的状态是否为待支付。如果是待支付状态，则会更新订单状态为取消状态。如果不是待支付状态，说明该订单已经支付过了，则直接返回。</p><p>还有个关键点，用户完成支付之后，会修改订单状态为已支付。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060014249.png" alt="image-20220706001431192" style="zoom:50%;"><h2 id="_9-如何限流" tabindex="-1">9 如何限流？ <a class="header-anchor" href="#_9-如何限流" aria-label="Permalink to &quot;9 如何限流？&quot;">​</a></h2><p>通过秒杀活动，如果我们运气爆棚，可能会用非常低的价格买到不错的商品（这种概率堪比买福利彩票中大奖）。</p><p>但有些高手，并不会像我们一样老老实实，通过秒杀页面点击秒杀按钮，抢购商品。他们可能在自己的服务器上，模拟正常用户登录系统，跳过秒杀页面，直接调用秒杀接口。</p><p>如果是我们手动操作，一般情况下，一秒钟只能点击一次秒杀按钮。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207052334120.png" alt="image-20220705233451042" style="zoom:67%;"><p>但是如果是服务器，一秒钟可以请求成上千接口。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207052335401.png" alt="image-20220705233516333" style="zoom:67%;"><p>这种差距实在太明显了，如果不做任何限制，绝大部分商品可能是被机器抢到，而非正常的用户，有点不太公平。</p><p>所以，我们有必要识别这些非法请求，做一些限制。那么，我们该如何现在这些非法请求呢？</p><p>目前有两种常用的限流方式：</p><ol><li>基于nginx限流</li><li>基于redis限流</li></ol><h3 id="_9-1-对同一用户限流" tabindex="-1">9.1 对同一用户限流 <a class="header-anchor" href="#_9-1-对同一用户限流" aria-label="Permalink to &quot;9.1 对同一用户限流&quot;">​</a></h3><p>为了防止某个用户，请求接口次数过于频繁，可以只针对该用户做限制。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207052335609.png" alt="image-20220705233554517" style="zoom:50%;"><p>限制同一个用户id，比如每分钟只能请求5次接口。</p><h3 id="_9-2-对同一ip限流" tabindex="-1">9.2 对同一ip限流 <a class="header-anchor" href="#_9-2-对同一ip限流" aria-label="Permalink to &quot;9.2 对同一ip限流&quot;">​</a></h3><p>有时候只对某个用户限流是不够的，有些高手可以模拟多个用户请求，这种nginx就没法识别了。</p><p>这时需要加同一ip限流功能。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060011452.png" alt="image-20220706001147365" style="zoom:50%;"><p>限制同一个ip，比如每分钟只能请求5次接口。</p><p>但这种限流方式可能会有误杀的情况，比如同一个公司或网吧的出口ip是相同的，如果里面有多个正常用户同时发起请求，有些用户可能会被限制住。</p><h3 id="_9-3-对接口限流" tabindex="-1">9.3 对接口限流 <a class="header-anchor" href="#_9-3-对接口限流" aria-label="Permalink to &quot;9.3 对接口限流&quot;">​</a></h3><p>别以为限制了用户和ip就万事大吉，有些高手甚至可以使用代理，每次都请求都换一个ip。</p><p>这时可以限制请求的接口总次数。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207060012107.png" alt="image-20220706001229020" style="zoom:50%;"><p>在高并发场景下，这种限制对于系统的稳定性是非常有必要的。但可能由于有些非法请求次数太多，达到了该接口的请求上限，而影响其他的正常用户访问该接口。看起来有点得不偿失。</p><h3 id="_9-4-加验证码" tabindex="-1">9.4 加验证码 <a class="header-anchor" href="#_9-4-加验证码" aria-label="Permalink to &quot;9.4 加验证码&quot;">​</a></h3><p>相对于上面三种方式，加验证码的方式可能更精准一些，同样能限制用户的访问频次，但好处是不会存在误杀的情况。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207052332881.png" alt="image-20220705233247783" style="zoom:33%;"><p>通常情况下，用户在请求之前，需要先输入验证码。用户发起请求之后，服务端会去校验该验证码是否正确。只有正确才允许进行下一步操作，否则直接返回，并且提示验证码错误。</p><p>此外，验证码一般是一次性的，同一个验证码只允许使用一次，不允许重复使用。</p><p>普通验证码，由于生成的数字或者图案比较简单，可能会被破解。优点是生成速度比较快，缺点是有安全隐患。</p><p>还有一个验证码叫做：<code>移动滑块</code>，它生成速度比较慢，但比较安全，是目前各大互联网公司的首选。</p><h3 id="_9-5-提高业务门槛" tabindex="-1">9.5 提高业务门槛 <a class="header-anchor" href="#_9-5-提高业务门槛" aria-label="Permalink to &quot;9.5 提高业务门槛&quot;">​</a></h3><p>上面说的加验证码虽然可以限制非法用户请求，但是有些影响用户体验。用户点击秒杀按钮前，还要先输入验证码，流程显得有点繁琐，秒杀功能的流程不是应该越简单越好吗？</p><p>其实，有时候达到某个目的，不一定非要通过技术手段，通过业务手段也一样。</p><p>12306刚开始的时候，全国人民都在同一时刻抢火车票，由于并发量太大，系统经常挂。后来，重构优化之后，将购买周期放长了，可以提前20天购买火车票，并且可以在9点、10、11点、12点等整点购买火车票。调整业务之后（当然技术也有很多调整），将之前集中的请求，分散开了，一下子降低了用户并发量。</p><p>回到这里，我们通过提高业务门槛，比如只有会员才能参与秒杀活动，普通注册用户没有权限。或者，只有等级到达3级以上的普通用户，才有资格参加该活动。</p><p>这样简单的提高一点门槛，即使是黄牛党也束手无策，他们总不可能为了参加一次秒杀活动，还另外花钱充值会员吧？</p><h1 id="详解4种经典的限流算法" tabindex="-1">详解4种经典的限流算法 <a class="header-anchor" href="#详解4种经典的限流算法" aria-label="Permalink to &quot;详解4种经典的限流算法&quot;">​</a></h1><p>最近，我们的业务系统引入了Guava的<strong>RateLimiter</strong>限流组件，它是基于<strong>令牌桶算法</strong>实现的,而令牌桶是非常经典的限流算法。本文将跟大家一起学习几种经典的限流算法。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061435455.png" alt="image-20220706143546405" style="zoom:67%;"><h2 id="限流是什么" tabindex="-1">限流是什么? <a class="header-anchor" href="#限流是什么" aria-label="Permalink to &quot;限流是什么?&quot;">​</a></h2><p>维基百科的概念如下：</p><p>在计算机网络中，<code>限流就是控制网络接口发送或接收请求的速率，它可防止DoS攻击和限制Web爬虫</code></p><p>限流，也称流量控制。是指系统在面临高并发，或者<strong>大流量请求</strong>的情况下，<strong>限制新的请求对系统的访问</strong>，从而<strong>保证系统的稳定性</strong>。限流会导致部分用户请求处理不及时或者被拒，这就影响了用户体验。所以一般需要在系统稳定和用户体验之间<strong>平衡</strong>一下。举个生活的例子：</p><blockquote><p>一些热门的旅游景区，一般会对每日的旅游参观人数有限制的。每天只会卖出固定数目的门票，比如5000张。假设在五一、国庆假期，你去晚了，可能当天的票就已经卖完了，就无法进去游玩了。即使你进去了，排队也能排到你怀疑人生。</p></blockquote><h2 id="常见的限流算法" tabindex="-1">常见的限流算法 <a class="header-anchor" href="#常见的限流算法" aria-label="Permalink to &quot;常见的限流算法&quot;">​</a></h2><h3 id="固定窗口限流算法" tabindex="-1">固定窗口限流算法 <a class="header-anchor" href="#固定窗口限流算法" aria-label="Permalink to &quot;固定窗口限流算法&quot;">​</a></h3><h4 id="算法分析" tabindex="-1">算法分析 <a class="header-anchor" href="#算法分析" aria-label="Permalink to &quot;算法分析&quot;">​</a></h4><p>首先维护一个计数器，<code>将单位时间段当做一个窗口，计数器记录这个窗口接收请求的次数</code>。</p><ul><li><code>当次数少于限流阀值，就允许访问，并且计数器+1</code></li><li><code>当次数大于限流阀值，就拒绝访问</code></li><li><code>当前的时间窗口过去之后，计数器清零</code></li></ul><p>假设单位时间是1秒，限流阀值为3。在单位时间1秒内，每来一个请求,计数器就加1，如果计数器累加的次数超过限流阀值3，后续的请求全部拒绝。等到1s结束后，计数器清0，重新开始计数。如下图：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061437099.png" alt="image-20220706143721031" style="zoom:67%;"><h4 id="代码实现" tabindex="-1">代码实现 <a class="header-anchor" href="#代码实现" aria-label="Permalink to &quot;代码实现&quot;">​</a></h4><p>下面是简单的代码实现，QPS 限制为 2，这里的代码做了一些<strong>优化</strong>，并没有单独开一个线程去每隔 1 秒重置计数器，而是在每次调用时进行时间间隔计算来确定是否先重置计数器。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#676E95;font-style:italic;">// 阈值</span></span>
<span class="line"><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">final</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Integer</span><span style="color:#A6ACCD;"> QPS </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">2</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">// 时间窗口（毫秒）</span></span>
<span class="line"><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">final</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">long</span><span style="color:#A6ACCD;"> TIME_WINDOWS </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1000</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">// 计数器</span></span>
<span class="line"><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">final</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">AtomicInteger</span><span style="color:#A6ACCD;"> REQ_COUNT </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">new</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">AtomicInteger</span><span style="color:#89DDFF;">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">long</span><span style="color:#A6ACCD;"> START_TIME </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> System</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">currentTimeMillis</span><span style="color:#89DDFF;">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">synchronized</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">((</span><span style="color:#A6ACCD;">System</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">currentTimeMillis</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;"> START_TIME</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> TIME_WINDOWS</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        REQ_COUNT</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        START_TIME </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> System</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">currentTimeMillis</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> REQ_COUNT</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">incrementAndGet</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&lt;=</span><span style="color:#A6ACCD;"> QPS</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">t1</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> throws Exception </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">for</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> i </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span><span style="color:#A6ACCD;"> i </span><span style="color:#89DDFF;">&lt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">10</span><span style="color:#89DDFF;">;</span><span style="color:#A6ACCD;"> i</span><span style="color:#89DDFF;">++)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        Thread</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">sleep</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">250</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">LocalTime</span><span style="color:#A6ACCD;"> now </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> LocalTime</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">now</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(!</span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">())</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">            System</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">out</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">println</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">now </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;"> 被限流</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">            System</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">out</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">println</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">now </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;"> 做点什么</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>运行结果：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.5.13/202205141003287.png" alt="image-20220514100310224" style="zoom:80%;"><p>从输出结果中可以看到大概每秒操作 3 次，由于限制 QPS 为 2，所以平均会有一次被限流。看起来可以了，不过我们思考一下就会发现这种简单的限流方式是有问题的，虽然我们限制了 QPS 为 2，但是当遇到时间窗口的临界突变时，如 1s 中的后 500 ms 和第 2s 的前 500ms 时，虽然是加起来是 1s 时间，却可以被请求 4 次。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.5.13/202205141005141.png" alt="image-20220514100505083" style="zoom:80%;"><p>简单修改测试代码，可以进行验证：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">Thread</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">sleep</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">400</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">for</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> i </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span><span style="color:#A6ACCD;"> i </span><span style="color:#89DDFF;">&lt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">10</span><span style="color:#89DDFF;">;</span><span style="color:#A6ACCD;"> i</span><span style="color:#89DDFF;">++)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    Thread</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">sleep</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">250</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">LocalTime</span><span style="color:#A6ACCD;"> now </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> LocalTime</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">now</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(!</span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">())</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        System</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">out</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">println</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">now </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">被限流</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">else</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        System</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">out</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">println</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">now </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">做点什么</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>得到输出中可以看到连续 4 次请求，间隔 250 ms 没有却被限制。：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.5.13/202205141007270.png" alt="image-20220514100742213" style="zoom:80%;"><p>但是，这种算法有一个很明显的<strong>临界问题</strong>：假设限流阀值为5个请求，单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s，分别并发5个请求。虽然都没有超过阀值，但是如果算0.8-1.2s,则并发数高达10，已经<strong>超过单位时间1s不超过5阀值</strong>的定义啦。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061439822.png" alt="image-20220706143905764" style="zoom:67%;"><h3 id="滑动窗口限流算法" tabindex="-1">滑动窗口限流算法 <a class="header-anchor" href="#滑动窗口限流算法" aria-label="Permalink to &quot;滑动窗口限流算法&quot;">​</a></h3><p>滑动窗口限流解决固定窗口临界值的问题。它将单位时间周期分为n个小周期，分别记录每个小周期内接口的访问次数，并且根据时间滑动删除过期的小周期。</p><p>一张图解释滑动窗口算法，如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061439636.png" alt="image-20220706143938576" style="zoom:50%;"><p>假设单位时间还是1s，滑动窗口算法把它划分为5个小周期，也就是滑动窗口（单位时间）被划分为5个小格子。每格表示0.2s。每过0.2s，时间窗口就会往右滑动一格。然后呢，每个小周期，都有自己独立的计数器，如果请求是0.83s到达的，0.8~1.0s对应的计数器就会加1。</p><p>我们来看下滑动窗口是如何解决临界问题的？</p><p>假设我们1s内的限流阀值还是5个请求，0.8~1.0s内（比如0.9s的时候）来了5个请求，落在黄色格子里。时间过了1.0s这个点之后，又来5个请求，落在紫色格子里。如果<strong>是固定窗口算法，是不会被限流的</strong>，但是<strong>滑动窗口的话，每过一个小周期，它会右移一个小格</strong>。过了1.0s这个点后，会右移一小格，当前的单位时间段是0.2~1.2s，这个区域的请求已经超过限定的5了，已触发限流啦，实际上，紫色格子的请求都被拒绝啦。</p><p><strong>TIPS:</strong> 当滑动窗口的格子周期划分的越多，那么滑动窗口的滚动就越平滑，限流的统计就会越精确。</p><p>滑动窗口算法虽然解决了<strong>固定窗口的临界问题</strong>，但是一旦到达限流后，请求都会直接暴力被拒绝。酱紫我们会损失一部分请求，这其实对于产品来说，并不太友好。</p><h3 id="漏桶算法" tabindex="-1">漏桶算法 <a class="header-anchor" href="#漏桶算法" aria-label="Permalink to &quot;漏桶算法&quot;">​</a></h3><p>漏桶算法面对限流，就更加的柔性，不存在直接的粗暴拒绝。</p><p>它的原理很简单，可以认为就是<strong>注水漏水</strong>的过程。往漏桶中以任意速率流入水，以固定的速率流出水。当水超过桶的容量时，会被溢出，也就是被丢弃。因为桶容量是不变的，保证了整体的速率。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061441418.png" alt="image-20220706144136345" style="zoom:50%;"><ul><li>流入的水滴，可以看作是访问系统的请求，这个流入速率是不确定的。</li><li>桶的容量一般表示系统所能处理的请求数。</li><li>如果桶的容量满了，就达到限流的阀值，就会丢弃水滴（拒绝请求）</li><li>流出的水滴，是恒定过滤的，对应服务按照固定的速率处理请求。</li></ul><p>在正常流量的时候，系统按照固定的速率处理请求，是我们想要的。但是<strong>面对突发流量</strong>的时候，漏桶算法还是循规蹈矩地处理请求，这就不是我们想看到的啦。流量变突发时，我们肯定<strong>希望系统尽量快点处理请求</strong>，提升用户体验嘛。</p><h3 id="令牌桶算法-推荐" tabindex="-1">令牌桶算法(推荐) <a class="header-anchor" href="#令牌桶算法-推荐" aria-label="Permalink to &quot;令牌桶算法(推荐)&quot;">​</a></h3><h4 id="算法讲解" tabindex="-1">算法讲解 <a class="header-anchor" href="#算法讲解" aria-label="Permalink to &quot;算法讲解&quot;">​</a></h4><p>面对<strong>突发流量</strong>的时候，我们可以使用令牌桶算法限流。</p><p><strong>令牌桶算法原理</strong>：</p><ul><li>有一个令牌管理员，根据限流大小，定速往令牌桶里放令牌。</li><li>如果令牌数量满了，超过令牌桶容量的限制，那就丢弃。</li><li>系统在接受到一个用户请求时，都会先去令牌桶要一个令牌。如果拿到令牌，那么就处理这个请求的业务逻辑；</li><li>如果拿不到令牌，就直接拒绝这个请求。</li></ul><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061440577.png" alt="image-20220706144052495" style="zoom:50%;"><p>如果令牌发放的策略正确，这个系统即不会被拖垮，也能提高机器的利用率。Guava的RateLimiter限流组件，就是基于<strong>令牌桶算法</strong>实现的。</p><h4 id="guava代码实现" tabindex="-1">Guava代码实现 <a class="header-anchor" href="#guava代码实现" aria-label="Permalink to &quot;Guava代码实现&quot;">​</a></h4><p>Google 的 Java 开发工具包 Guava 中的限流工具类 RateLimiter 就是令牌桶的一个实现，日常开发中我们也不会手动实现了，这里直接使用 RateLimiter 进行测试。</p><div class="language-xml"><button title="Copy Code" class="copy"></button><span class="lang">xml</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">dependency</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">groupId</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;">com.google.guava</span><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">groupId</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">artifactId</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;">guava</span><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">artifactId</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">version</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;">30.1-jre</span><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">version</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">dependency</span><span style="color:#89DDFF;">&gt;</span></span></code></pre></div><h5 id="简单示例" tabindex="-1">简单示例 <a class="header-anchor" href="#简单示例" aria-label="Permalink to &quot;简单示例&quot;">​</a></h5><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">static</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">t1</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> throws InterruptedException </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">// qps 2</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">RateLimiter</span><span style="color:#A6ACCD;"> rateLimiter </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> RateLimiter</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">create</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">2</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">for</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">int</span><span style="color:#A6ACCD;"> i </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">0</span><span style="color:#89DDFF;">;</span><span style="color:#A6ACCD;"> i </span><span style="color:#89DDFF;">&lt;</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">10</span><span style="color:#89DDFF;">;</span><span style="color:#A6ACCD;"> i</span><span style="color:#89DDFF;">++)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> time </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> LocalDateTime</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">now</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">format</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">DateTimeFormatter</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">ISO_LOCAL_TIME</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        System</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">out</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">println</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">time </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">:</span><span style="color:#89DDFF;">&quot;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> rateLimiter</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">());</span></span>
<span class="line"><span style="color:#A6ACCD;">        Thread</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">sleep</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">250</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>代码中限制 QPS 为 2，也就是每隔 500ms 生成一个令牌，但是程序每隔 250ms 获取一次令牌，所以两次获取中只有一次会成功。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.5.13/202205141046452.png" alt="image-20220514104613398" style="zoom:80%;"><p>虽然演示了 Google Guava 工具包中的 RateLimiter 的实现，但是我们需要思考一个问题，就是<code>令牌的添加方式</code>，如果按照指定间隔添加令牌，那么需要开一个线程去定时添加，如果有很多个接口很多个 RateLimiter 实例，<strong>线程数会随之增加</strong>，这显然不是一个好的办法。显然 Google 也考虑到了这个问题，<code>在 RateLimiter 中，是在每次令牌获取时才进行计算令牌是否足够的。它通过存储的下一个令牌生成的时间，和当前获取令牌的时间差，再结合阈值，去计算令牌是否足够，同时再记录下一个令牌的生成时间以便下一次调用</code>。</p><p>下面是 Guava 中 RateLimiter 类的子类 SmoothRateLimiter 的 <code>resync()</code> 方法的代码分析，可以看到其中的令牌计算逻辑。</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">resync</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">long</span><span style="color:#A6ACCD;"> nowMicros</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span><span style="color:#A6ACCD;"> </span><span style="color:#676E95;font-style:italic;">// 当前微秒时间</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">// 当前时间是否大于下一个令牌生成时间</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">nowMicros </span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">this.</span><span style="color:#A6ACCD;">nextFreeTicketMicros</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#89DDFF;">       </span><span style="color:#676E95;font-style:italic;">// 可生成的令牌数 newPermits = （当前时间 - 下一个令牌生成时间）/ 令牌生成时间间隔。</span></span>
<span class="line"><span style="color:#89DDFF;">       </span><span style="color:#676E95;font-style:italic;">// 如果 QPS 为2，这里的 coolDownIntervalMicros 就是 500000.0 微秒(500ms)</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">double</span><span style="color:#A6ACCD;"> newPermits </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">double</span><span style="color:#89DDFF;">)(</span><span style="color:#A6ACCD;">nowMicros </span><span style="color:#89DDFF;">-</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">this.</span><span style="color:#A6ACCD;">nextFreeTicketMicros</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">/</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;">this.</span><span style="color:#82AAFF;">coolDownIntervalMicros</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">// 更新令牌库存 storedPermits。</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#89DDFF;">this.</span><span style="color:#A6ACCD;">storedPermits </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> Math</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">min</span><span style="color:#89DDFF;">(this.</span><span style="color:#A6ACCD;">maxPermits</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">this.</span><span style="color:#A6ACCD;">storedPermits </span><span style="color:#89DDFF;">+</span><span style="color:#A6ACCD;"> newPermits</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">// 更新下一个令牌生成时间 nextFreeTicketMicros</span></span>
<span class="line"><span style="color:#A6ACCD;">       </span><span style="color:#89DDFF;">this.</span><span style="color:#A6ACCD;">nextFreeTicketMicros </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> nowMicros</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><h5 id="限流实现" tabindex="-1">限流实现 <a class="header-anchor" href="#限流实现" aria-label="Permalink to &quot;限流实现&quot;">​</a></h5><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">com</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">google</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">common</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">util</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">concurrent</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">RateLimiter</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">lombok</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">extern</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">slf4j</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Slf4j</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">springframework</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">web</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">bind</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">GetMapping</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">springframework</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">web</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">bind</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">RequestMapping</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">springframework</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">web</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">bind</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">RestController</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">time</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">LocalDateTime</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">time</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">format</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">DateTimeFormatter</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">util</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">concurrent</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">TimeUnit</span><span style="color:#89DDFF;">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Slf4j</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RestController</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RequestMapping</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">/limit</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">class</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">LimitController</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">    </span><span style="color:#676E95;font-style:italic;">//限流策略 ：1秒钟2个请求</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">final</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">RateLimiter</span><span style="color:#A6ACCD;"> limiter </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> RateLimiter</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">create</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">2.0</span><span style="color:#89DDFF;">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">DateTimeFormatter</span><span style="color:#A6ACCD;"> dtf </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">DateTimeFormatter</span></span>
<span class="line"><span style="color:#A6ACCD;">                                    </span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">ofPattern</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">yyyy-MM-dd HH:mm:ss</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">GetMapping</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">/test1</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">testLimiter</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">        </span><span style="color:#676E95;font-style:italic;">//50毫秒内，没拿到令牌，就直接进入服务降级</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> tryAcquire </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> limiter</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">50</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">MILLISECONDS</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">        </span><span style="color:#676E95;font-style:italic;">//没有获取令牌</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(!</span><span style="color:#A6ACCD;">tryAcquire</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">            log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">warn</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">进入服务降级，时间{}</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> LocalDateTime</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">now</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">format</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">dtf</span><span style="color:#89DDFF;">));</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">当前排队人数较多，请稍后再试！</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">        </span><span style="color:#676E95;font-style:italic;">//成功获取令牌</span></span>
<span class="line"><span style="color:#A6ACCD;">        log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">info</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">获取令牌成功，时间{}</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> LocalDateTime</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">now</span><span style="color:#89DDFF;">().</span><span style="color:#82AAFF;">format</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">dtf</span><span style="color:#89DDFF;">));</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">请求成功</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img/image-20220416164522623.png" alt="image-20220416164522623" style="zoom:67%;"><p>以上用到了RateLimiter的2个核心方法：<code>create()</code>、<code>tryAcquire()</code>，以下为详细说明</p><ul><li>acquire() 获取一个令牌, 改方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间</li><li>acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间</li><li>tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 false</li><li>tryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 false</li><li>tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 false</li><li>tryAcquire(int permits, long timeout, TimeUnit unit) 同上</li></ul><p>从以上日志可以看出，1秒钟内只有2次成功，其他都失败降级了，说明我们已经成功给接口加上了限流功能。</p><p>当然了，我们在实际开发中并不能直接这样用。至于原因嘛，你想呀，你每个接口都需要手动给其加上<code>tryAcquire()</code>，<code>业务代码和限流代码混在一起，而且明显违背了DRY原则，代码冗余，重复劳动</code>。代码评审时肯定会被老鸟们给嘲笑一番，啥破玩意儿！</p><h2 id="基于aop接口限流-重要" tabindex="-1">基于AOP接口限流(重要) <a class="header-anchor" href="#基于aop接口限流-重要" aria-label="Permalink to &quot;基于AOP接口限流(重要)&quot;">​</a></h2><p>基于AOP的实现方式也非常简单，实现过程如下：</p><h3 id="第一步-加入aop依赖" tabindex="-1">第一步：加入AOP依赖 <a class="header-anchor" href="#第一步-加入aop依赖" aria-label="Permalink to &quot;第一步：加入AOP依赖&quot;">​</a></h3><div class="language-xml"><button title="Copy Code" class="copy"></button><span class="lang">xml</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">dependency</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">groupId</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;">org.springframework.boot</span><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">groupId</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">&lt;</span><span style="color:#F07178;">artifactId</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;">spring-boot-starter-aop</span><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">artifactId</span><span style="color:#89DDFF;">&gt;</span></span>
<span class="line"><span style="color:#89DDFF;">&lt;/</span><span style="color:#F07178;">dependency</span><span style="color:#89DDFF;">&gt;</span></span></code></pre></div><h3 id="第二步-自定义限流注解" tabindex="-1">第二步：自定义限流注解 <a class="header-anchor" href="#第二步-自定义限流注解" aria-label="Permalink to &quot;第二步：自定义限流注解&quot;">​</a></h3><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">lang</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">*</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">util</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">concurrent</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">TimeUnit</span><span style="color:#89DDFF;">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Retention</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">RetentionPolicy</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">RUNTIME</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Target</span><span style="color:#89DDFF;">({</span><span style="color:#A6ACCD;">ElementType</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">METHOD</span><span style="color:#89DDFF;">})</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Documented</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">interface</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Limit</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#676E95;font-style:italic;">/**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 资源的key,唯一</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 作用：不同的接口，不同的流量控制</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">key</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">default</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;&quot;</span><span style="color:#89DDFF;">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#676E95;font-style:italic;">/**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 最多的访问限制次数</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">double</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">permitsPerSecond</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#676E95;font-style:italic;">/**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 获取令牌最大等待时间</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">long</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">timeout</span><span style="color:#89DDFF;">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#676E95;font-style:italic;">/**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">TimeUnit</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">timeunit</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">default</span><span style="color:#A6ACCD;"> TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">MILLISECONDS</span><span style="color:#89DDFF;">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#676E95;font-style:italic;">/**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 得不到令牌的提示语</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">msg</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">default</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">系统繁忙,请稍后再试.</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><h3 id="第三步-使用aop切面拦截限流注解" tabindex="-1">第三步：使用AOP切面拦截限流注解 <a class="header-anchor" href="#第三步-使用aop切面拦截限流注解" aria-label="Permalink to &quot;第三步：使用AOP切面拦截限流注解&quot;">​</a></h3><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">com</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">google</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">common</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">collect</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Maps</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">com</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">google</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">common</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">util</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">concurrent</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">RateLimiter</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">lombok</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">extern</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">slf4j</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Slf4j</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">aspectj</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">lang</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">ProceedingJoinPoint</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">aspectj</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">lang</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Around</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">aspectj</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">lang</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Aspect</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">aspectj</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">lang</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">reflect</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">MethodSignature</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">springframework</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">stereotype</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Component</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">lang</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">reflect</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Method</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">util</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Map</span><span style="color:#89DDFF;">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Slf4j</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Aspect</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Component</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">class</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">LimitAop</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">    /**</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * 不同的接口，不同的流量控制</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     * map的key为 Limiter.key</span></span>
<span class="line"><span style="color:#676E95;font-style:italic;">     */</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">final</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Map</span><span style="color:#89DDFF;">&lt;</span><span style="color:#C792EA;">String</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">RateLimiter</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> limitMap </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> Maps</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">newConcurrentMap</span><span style="color:#89DDFF;">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Around</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">@annotation(com.it.config.Limit)</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Object</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">around</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">ProceedingJoinPoint</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">joinPoint</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">throws</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Throwable</span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">MethodSignature</span><span style="color:#A6ACCD;"> signature </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">MethodSignature</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> joinPoint</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getSignature</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">Method</span><span style="color:#A6ACCD;"> method </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> signature</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getMethod</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#89DDFF;">        </span><span style="color:#676E95;font-style:italic;">//拿limit的注解</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">Limit</span><span style="color:#A6ACCD;"> limit </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> method</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getAnnotation</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">Limit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">class</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">limit </span><span style="color:#89DDFF;">!=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">null)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">            </span><span style="color:#676E95;font-style:italic;">//key作用：不同的接口，不同的流量控制</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> key</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">limit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">key</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#C792EA;">RateLimiter</span><span style="color:#A6ACCD;"> rateLimiter </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">null;</span></span>
<span class="line"><span style="color:#89DDFF;">            </span><span style="color:#676E95;font-style:italic;">//验证缓存是否有命中key</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(!</span><span style="color:#A6ACCD;">limitMap</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">containsKey</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">key</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#89DDFF;">                </span><span style="color:#676E95;font-style:italic;">// 创建令牌桶</span></span>
<span class="line"><span style="color:#A6ACCD;">                rateLimiter </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> RateLimiter</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">create</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">limit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">permitsPerSecond</span><span style="color:#89DDFF;">());</span></span>
<span class="line"><span style="color:#A6ACCD;">                limitMap</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">put</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">key</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> rateLimiter</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">                log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">info</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">新建了令牌桶={}，容量={}</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">key</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">limit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">permitsPerSecond</span><span style="color:#89DDFF;">());</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">            rateLimiter </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> limitMap</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">get</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">key</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">            </span><span style="color:#676E95;font-style:italic;">// 拿令牌</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#C792EA;">boolean</span><span style="color:#A6ACCD;"> acquire </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> rateLimiter</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">tryAcquire</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">limit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">timeout</span><span style="color:#89DDFF;">(),</span></span>
<span class="line"><span style="color:#A6ACCD;">                                                     limit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">timeunit</span><span style="color:#89DDFF;">());</span></span>
<span class="line"><span style="color:#89DDFF;">            </span><span style="color:#676E95;font-style:italic;">// 拿不到命令，直接返回异常提示</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(!</span><span style="color:#A6ACCD;">acquire</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">                log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">info</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">令牌桶={}，获取令牌失败</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">key</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">                </span><span style="color:#676E95;font-style:italic;">// this.responseFail(limit.msg());</span></span>
<span class="line"><span style="color:#A6ACCD;">                log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">info</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">limit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">msg</span><span style="color:#89DDFF;">());</span></span>
<span class="line"><span style="color:#A6ACCD;">                </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">null;</span></span>
<span class="line"><span style="color:#A6ACCD;">            </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> joinPoint</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">proceed</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><h3 id="第四步-给需要限流的接口加上注解" tabindex="-1">第四步：给需要限流的接口加上注解 <a class="header-anchor" href="#第四步-给需要限流的接口加上注解" aria-label="Permalink to &quot;第四步：给需要限流的接口加上注解&quot;">​</a></h3><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">com</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">it</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">config</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Limit</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">lombok</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">extern</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">slf4j</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">Slf4j</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">springframework</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">web</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">bind</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">GetMapping</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">springframework</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">web</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">bind</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">RequestMapping</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">org</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">springframework</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">web</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">bind</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">annotation</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">RestController</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#F78C6C;">import</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">java</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">util</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">concurrent</span><span style="color:#89DDFF;">.</span><span style="color:#C792EA;">TimeUnit</span><span style="color:#89DDFF;">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Slf4j</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RestController</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RequestMapping</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">/limit</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">class</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">LimitController</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">GetMapping</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">/test2</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Limit</span><span style="color:#89DDFF;">(</span><span style="color:#FFCB6B;">key</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">limit2</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">permitsPerSecond</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">timeout</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">50</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;">           </span><span style="color:#FFCB6B;">timeunit</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">MILLISECONDS</span><span style="color:#89DDFF;">,</span><span style="color:#FFCB6B;">msg</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">当前排队人数较多，请稍后再试！</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">limit2</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">info</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">令牌桶limit2获取令牌成功</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">ok</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">GetMapping</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">/test3</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Limit</span><span style="color:#89DDFF;">(</span><span style="color:#FFCB6B;">key</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">limit3</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">permitsPerSecond</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">2</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">timeout</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">50</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;">           </span><span style="color:#FFCB6B;">timeunit</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> TimeUnit</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">MILLISECONDS</span><span style="color:#89DDFF;">,</span><span style="color:#FFCB6B;">msg</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">系统繁忙，请稍后再试！</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">limit3</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">info</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">令牌桶limit3获取令牌成功</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">ok</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img/image-20220416172542468.png" alt="image-20220416172542468" style="zoom:67%;"><h1 id="高并发系统3大利器之缓存" tabindex="-1">高并发系统3大利器之缓存 <a class="header-anchor" href="#高并发系统3大利器之缓存" aria-label="Permalink to &quot;高并发系统3大利器之缓存&quot;">​</a></h1><h2 id="引言" tabindex="-1">引言 <a class="header-anchor" href="#引言" aria-label="Permalink to &quot;引言&quot;">​</a></h2><p>随着互联网的高速发展，市面上也出现了越来越多的网站和<code>app</code>。我们判断一个软件是否好用，用户体验就是一个重要的衡量标准。比如说我们经常用的微信，打开一个页面要十几秒，发个语音要几分钟对方才能收到。相信这样的软件大家肯定是都不愿意用的。软件要做到用户体验好，响应速度快，缓存就是必不可少的一个神器。缓存又分进程内缓存和分布式缓存两种：分布式缓存如<code>redis</code>、<code>memcached</code>等，还有本地（进程内）缓存如<code>ehcache</code>、<code>GuavaCache</code>、<code>Caffeine</code>等。</p><h2 id="缓存特征" tabindex="-1">缓存特征 <a class="header-anchor" href="#缓存特征" aria-label="Permalink to &quot;缓存特征&quot;">​</a></h2><p>缓存作为一个数据数据模型对象，那么它有一些什么样的特征呢？下面我们分别来介绍下这些特征。</p><h3 id="命中率" tabindex="-1">命中率 <a class="header-anchor" href="#命中率" aria-label="Permalink to &quot;命中率&quot;">​</a></h3><ul><li>命中率=命中数/（命中数+没有命中数）当某个请求能够通过访问缓存而得到响应时，称为缓存命中。缓存命中率越高，缓存的利用率也就越高。</li></ul><h3 id="最大空间" tabindex="-1">最大空间 <a class="header-anchor" href="#最大空间" aria-label="Permalink to &quot;最大空间&quot;">​</a></h3><ul><li>缓存中可以容纳最大元素的数量。当缓存存放的数据超过最大空间时，就需要根据淘汰算法来淘汰部分数据存放新到达的数据。</li></ul><h3 id="淘汰算法" tabindex="-1">淘汰算法 <a class="header-anchor" href="#淘汰算法" aria-label="Permalink to &quot;淘汰算法&quot;">​</a></h3><ul><li>缓存的存储空间有限制，当缓存空间被用满时，如何保证在稳定服务的同时有效提升命中率？这就由缓存淘汰算法来处理，设计适合自身数据特征的淘汰算法能够有效提升缓存命中率。常见的淘汰算法有：</li></ul><h5 id="fifo-first-in-first-out" tabindex="-1">FIFO(first in first out) <a class="header-anchor" href="#fifo-first-in-first-out" aria-label="Permalink to &quot;FIFO(first in first out)&quot;">​</a></h5><ul><li><strong>「先进先出」</strong>。最先进入缓存的数据在缓存空间不够的情况下（超出最大元素限制）会被优先被清除掉，以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。<strong>「适用于保证高频数据有效性场景，优先保障最新数据可用」</strong>。</li></ul><h5 id="lfu-less-frequently-used" tabindex="-1">LFU(less frequently used) <a class="header-anchor" href="#lfu-less-frequently-used" aria-label="Permalink to &quot;LFU(less frequently used)&quot;">​</a></h5><ul><li><strong>「最少使用」</strong>，无论是否过期，根据元素的被使用次数判断，清除使用次数较少的元素释放空间。策略算法主要比较元素的<code>hitCount</code>（命中次数）。<strong>「适用于保证高频数据有效性场景」</strong>。</li></ul><h5 id="lru-least-recently-used" tabindex="-1">LRU(least recently used) <a class="header-anchor" href="#lru-least-recently-used" aria-label="Permalink to &quot;LRU(least recently used)&quot;">​</a></h5><ul><li><strong>「最近最少使用」</strong>，无论是否过期，根据元素最后一次被使用的时间戳，清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。<strong>「比较适用于热点数据场景，优先保证热点数据的有效性。」</strong></li></ul><h2 id="进程缓存" tabindex="-1">进程缓存 <a class="header-anchor" href="#进程缓存" aria-label="Permalink to &quot;进程缓存&quot;">​</a></h2><h4 id="为什么需要引入本地缓存-本地缓存的应用场景有哪些" tabindex="-1">为什么需要引入本地缓存，本地缓存的应用场景有哪些？ <a class="header-anchor" href="#为什么需要引入本地缓存-本地缓存的应用场景有哪些" aria-label="Permalink to &quot;为什么需要引入本地缓存，本地缓存的应用场景有哪些？&quot;">​</a></h4><p>本地缓存的话是我们的应用和缓存都在同一个进程里面，获取缓存数据的时候纯内存操作，没有额外的网络开销，速度非常快。它适用于缓存一些应用中基本不会变化的数据，比如（国家、省份、城市等）。</p><h4 id="项目中一般如何使用、怎么样加载、怎么样更新" tabindex="-1">项目中一般如何使用、怎么样加载、怎么样更新？ <a class="header-anchor" href="#项目中一般如何使用、怎么样加载、怎么样更新" aria-label="Permalink to &quot;项目中一般如何使用、怎么样加载、怎么样更新？&quot;">​</a></h4><p>进程缓存的话，一般可以在应用启动的时候，把需要的数据加载到系统中。更新缓存的话可以采取定时更新（实时性不高）。具体实现的话就是在应用中起一个定时任务（<strong>「ScheduledExecutorService」</strong>、**「TimerTask」<strong>等），让它每隔多久去加载变更（数据变更之后可以修改数据库最后修改的时间，每次查询变更数据的时候都可以根据这个最后变更时间加上半小时大于当前时间的数据）的数据重新到缓存里面来。如果觉得这个比较麻烦的话，还可以直接全部全量更新（就跟项目启动加载数据一样）。这种方式的话，对数据更新可能会有点延迟。可能这台机器看到的是更新后的数据，那台机器看到的数据还是老的（机器发布时间可能不一样）。所以这种方式比较适用于对数据实时性要求不高的数据。如果对实时性有要求的话可以通过广播订阅<code>mq</code>消息。如果有数据更新<code>mq</code>会把更新数据推送到每一台机器，这种方式的话实时性会比前一种</strong>「定时更新」**的方法会好。但是实现起来会比较复杂。<img src="https://mmbiz.qpic.cn/mmbiz_png/qu3ItokgsArCVrK5pVwGyjBephk6KibicsXic0yRke9qYTz43Z3156GGMMK72xW8LNfXfu1kQIRx5zyIVleDzqtOQ/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><h4 id="本地缓存有哪些实现方式" tabindex="-1">本地缓存有哪些实现方式？ <a class="header-anchor" href="#本地缓存有哪些实现方式" aria-label="Permalink to &quot;本地缓存有哪些实现方式？&quot;">​</a></h4><p>常见本地缓存有以下几种实现方式：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061850845.png" alt="image-20220706185018760" style="zoom:67%;"><p>从上述表格我们看出性能最佳的是<code>Caffeine</code>。关于这个本地缓存的话我还是强烈推荐的，里面提供了丰富的<code>api</code>，以及各种各样的淘汰算法。如需了解更加详细的话可以看下以前写的这个篇文章<a href="https://mp.weixin.qq.com/s?__biz=MzIyMjQwMTgyNA==&amp;mid=2247483811&amp;idx=1&amp;sn=9d0b207044b5fe447169d630a7f77aab&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">《本地缓存性能之王Caffeine》</a>。</p><h4 id="本地缓存缺点" tabindex="-1">本地缓存缺点 <a class="header-anchor" href="#本地缓存缺点" aria-label="Permalink to &quot;本地缓存缺点&quot;">​</a></h4><ul><li>本地缓存与业务系统耦合在一起，应用之间无法直接共享缓存的内容。需要每个应用节点单独的维护自己的缓存。每个节点都需要一份一样的缓存，对服务器内存造成一种浪费。本地缓存机器重启、或者宕机都会丢失。</li></ul><h2 id="分布式缓存" tabindex="-1">分布式缓存 <a class="header-anchor" href="#分布式缓存" aria-label="Permalink to &quot;分布式缓存&quot;">​</a></h2><p>分布式缓存是与应用分离的缓存组件或服务，其最大的优点是自身就是一个独立的应用，与本地应用隔离，多个应用可直接的共享缓存。常见的分布式缓存有<code>redis</code>、<code>MemCache</code>等。</p><p>分布式缓存的应用</p><p>在高并发的环境下，比如春节抢票大战，一到放票的时间节点，分分钟大量用户以及黄牛的各种抢票软件流量进入<code>12306</code>，这时候如果每个用户的访问都去数据库实时查询票的库存，大量读的请求涌入到数据库，瞬间<code>Db</code>就会被打爆，<code>cpu</code>直接上升<code>100%</code>，服务马上就要宕机或者假死。即使进行了分库分表也是无法避免的。为了减轻db的压力以及提高系统的响应速度。一般都会在数据库前面加上一层缓存，甚至可能还会有多级缓存。</p><h2 id="缓存常见问题" tabindex="-1">缓存常见问题 <a class="header-anchor" href="#缓存常见问题" aria-label="Permalink to &quot;缓存常见问题&quot;">​</a></h2><h3 id="缓存雪崩" tabindex="-1">缓存雪崩 <a class="header-anchor" href="#缓存雪崩" aria-label="Permalink to &quot;缓存雪崩&quot;">​</a></h3><p>指大量缓存同一时间段集体失效，或者缓存整体不能提供服务，导致大量的请求全部到达数据库 对数据<code>CPU</code>和内存造成巨大压力，严重的会造成数据库宕机。因此而形成的一系列连锁反应造成整个系统奔溃。解决这个问题可以从以下方面入手：</p><ul><li>保证缓存的高可用。使用redis的集群模式，即使个别<code>redis</code>节点下线，缓存还是可以用。一般稍微大点的公司还可能会在多个机房部署Redis。这样即使某个机房突然停电，或者光纤又被挖断了，这时候缓存还是可以使用。</li><li>使用多级缓存。不同级别缓存时间过时时间不一样，即使某个级别缓存过期了，还有其他缓存级别 兜底。比如我们Redis缓存过期了，我们还有本地缓存。这样的话即使没有命中redis，有可能会命中本地缓存。</li><li>缓存永不过期。Redis中保存的key永久不失效，这样的话就不会出现大量缓存同时失效的问题，但是这种做法会浪费更多的存储空间，一般应该也不会推荐这种做法。</li><li>使用随机过期时间。为每一个key都合理的设计一个过期时间，这样可以避免大量的key在同一时刻集体失效。</li><li>异步重建缓存。这样的话需要维护每个<code>key</code>的过期时间，定时去轮询这些<code>key</code>的过期时间。例如一个<code>key</code>的<code>value</code>设置的过期时间是<code>30min</code>，那我们可以为这个key设置它自己的一个过期时间为<code>20min</code>。所以当这个<code>key</code>到了20min的时候我们就可以重新去构建这个<code>key</code>的缓存，同时也更新这个<code>key</code>的一个过期时间。</li></ul><h3 id="缓存穿透" tabindex="-1">缓存穿透 <a class="header-anchor" href="#缓存穿透" aria-label="Permalink to &quot;缓存穿透&quot;">​</a></h3><p>指查询一个不存在的数据，每次通过接口或者去查询数据库都查不到这个数据，比如黑客的恶意攻击，比如知道一个订单号后，然后就伪造一些不存在的订单号，然后并发来请求你这个订单详情。这些订单号在缓存中都查询不到，然后会导致把这些查询请求全部打到数据库或者SOA接口。这样的话就会导致数据库宕机或者你的服务大量超时。这种查询不存在的数据就是缓存击穿。解决这个问题可以从以下方面入手：</p><ul><li>缓存空值，对于这些不存在的请求，仍然给它缓存一个空的结果，这种方式简单粗暴，但是如果后续这个请求有新值了需要把原来缓存的空值删除掉（所以一般过期时间可以稍微设置的比较短）。</li><li>通过布隆过滤器。查询缓存之前先去布隆过滤器查询下这个数据是否存在。如果数据不存在，然后直接返回空。这样的话也会减少底层系统的查询压力。</li><li>缓存没有直接返回。这种方式的话要根据自己的实际业务来进行选择。比如固定的数据，一些省份信息或者城市信息，可以全部缓存起来。这样的话数据有变化的情况，缓存也需要跟着变化。实现起来可能比较复杂。</li></ul><h3 id="缓存击穿" tabindex="-1">缓存击穿 <a class="header-anchor" href="#缓存击穿" aria-label="Permalink to &quot;缓存击穿&quot;">​</a></h3><p>是指缓存里面的一个热点<code>key</code>(拼多多的五菱宏光神车的秒杀)在某个时间点过期。针对于这一个key有大量并发请求过来然后都会同时去数据库请求数据，瞬间对数据库造成巨大的压力。这个的话可以用缓存雪崩的几种解决方法来避免：</p><ul><li>缓存永不过期。Redis中保存的key永久不失效，这样的话就不会出现大量缓存同时失效的问题，但是这种做法会浪费更多的存储空间，一般应该也不会推荐这种做法。</li><li>异步重建缓存。这样的话需要维护每个<code>key</code>的过期时间，定时去轮询这些<code>key</code>的过期时间。例如一个<code>key</code>的<code>value</code>设置的过期时间是<code>30min</code>，那我们可以为这个key设置它自己的一个过期时间为<code>20min</code>。所以当这个key到了20min的时候我们就可以重新去构建这个<code>key</code>的缓存，同时也更新这个<code>key</code>的一个过期时间。</li><li>互斥锁重建缓存。这种情况的话只能针对于同一个<code>key</code>的情况下，比如你有<code>100</code>个并发请求都要来取<code>A</code>的缓存，这时候我们可以借助redis分布式锁来构建缓存，让只有一个请求可以去查询DB其他99个（没有获取到锁）都在外面等着，等A查询到数据并且把缓存构建好之后其他<code>99</code>个请求都只需要从缓存取就好了。原理就跟我们<code>java</code>的<code>DCL</code>（double checked locking）思想有点类似。</li></ul><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207061848588.png" alt="image-20220706184836527" style="zoom:50%;"><h2 id="缓存更新" tabindex="-1">缓存更新 <a class="header-anchor" href="#缓存更新" aria-label="Permalink to &quot;缓存更新&quot;">​</a></h2><p>我们一般的缓存更新主要有以下几种更新策略：</p><ul><li>先更新缓存，再更新数据库</li><li>先更新数据库，再更新缓存</li><li>先删除缓存，再更新数据库</li><li>先更新数据源库，再删除缓存 至于选择哪种更新策略的话，没有绝对的选择，可以根据自己的业务情况来选择适合自己的不过一般推荐的话是选择 <strong>「先更新数据源库，再删除缓存」</strong>。关于这几种更新的介绍可以推荐大家看下博客园大佬孤独烟写的《分布式之数据库和缓存双写一致性方案解析》这一篇文章，看完文章评论也可以去看看，评论跟内容一样精彩。</li></ul><h2 id="总结" tabindex="-1">总结 <a class="header-anchor" href="#总结" aria-label="Permalink to &quot;总结&quot;">​</a></h2><p>如果想要真正的设计好一个缓存，我们还是必须要掌握很多的知识，对于不同场景，缓存有各自不同的用法。比如实际工作中我们对于订单详情的一个缓存。我们可能会根据订单的状态来来构建缓存。我们就以机票订单为例，已出行、或者已经取消的订单我们基本上是不会去管的（订单状态已经终止了），这种的话数据基本也不会变了，所以对于这种订单我们设置的过期时间是不是就可以久一点，比如7天或者30天。对于未出行即将起飞的订单，这时候顾客是不是就会频繁的去刷新订单看看，看看有没有晚点什么的，或者登机口是在哪。对于这种实时性要求比较高的订单我们过期时间还是要设置的比较短的，如果是需要更改订单的状态查询的时候可以直接不走缓存，直接查询<code>master</code>库。毕竟这种更改订单状态的操作还是比较有限的。大多数情况都是用来展示的。展示的话是可以允许实时性要求没那么高。总的来说需要开具体的业务，没有通用的方案。看你的业务需求的容忍度，毕竟脱离了业务来谈技术都是耍流氓，是业务驱动技术。</p><h1 id="高并发下如何防重" tabindex="-1">高并发下如何防重 <a class="header-anchor" href="#高并发下如何防重" aria-label="Permalink to &quot;高并发下如何防重&quot;">​</a></h1><h2 id="前言" tabindex="-1">前言 <a class="header-anchor" href="#前言" aria-label="Permalink to &quot;前言&quot;">​</a></h2><p>最近测试给我提了一个bug，说我之前提供的一个批量复制商品的接口，产生了重复的商品数据。</p><p>追查原因之后发现，这个事情没想象中简单，可以说一波多折。</p><h2 id="_1-需求" tabindex="-1">1. 需求 <a class="header-anchor" href="#_1-需求" aria-label="Permalink to &quot;1. 需求&quot;">​</a></h2><p>产品有个需求：用户选择一些品牌，点击确定按钮之后，系统需要基于一份<code>默认</code>品牌的商品数据，复制出一批<code>新</code>的商品。</p><p>拿到这个需求时觉得太简单了，三下五除二就搞定。</p><p>我提供了一个复制商品的基础接口，给商城系统调用。</p><p>当时的流程图如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072312975.png" alt="image-20220707231240872" style="zoom:67%;"><p>如果每次复制的商品数量不多，使用同步接口调用的方案问题也不大。</p><h2 id="_2-性能优化" tabindex="-1">2. 性能优化 <a class="header-anchor" href="#_2-性能优化" aria-label="Permalink to &quot;2. 性能优化&quot;">​</a></h2><p>但由于每次需要复制的商品数量比较多，可能有几千。</p><p>如果每次都是用同步接口的方式复制商品，可能会有性能问题。</p><p>因此，后来我把复制商品的逻辑改成使用<code>mq</code>异步处理。</p><p>改造之后的流程图：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072312587.png" alt="image-20220707231259495" style="zoom:67%;"><p>复制商品的结果还需要通知商城系统：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072313683.png" alt="image-20220707231315563" style="zoom:67%;"><p>这个方案看起来，挺不错的。</p><p>但后来出现问题了。</p><h2 id="_3-出问题了" tabindex="-1">3. 出问题了 <a class="header-anchor" href="#_3-出问题了" aria-label="Permalink to &quot;3. 出问题了&quot;">​</a></h2><p>测试给我们提了一个bug，说我之前提供的一个批量复制商品的接口，产生了重复的商品数据。</p><p>经过追查之后发现，商城系统为了性能考虑，也改成异步了。</p><p>他们没有在接口中直接调用基础系统的复制商品接口，而是在<code>job</code>中调用的。</p><p>站在他们的视角流程图是这样的：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072313947.png" alt="image-20220707231336811" style="zoom:67%;"><p>用户调用商城的接口，他们会往<code>请求记录表</code>中写入一条数据，然后在另外一个<code>job</code>中，异步调用基础系统的接口去复制商品。</p><p>但实际情况是这样的：商城系统内部出现了bug，在请求记录表中，同一条请求产生了重复的数据。这样导致的结果是，在job中调用基础系统复制商品接口时，发送了重复的请求。</p><p>刚好基础系统现在是使用<code>RocketMQ</code>异步处理的。由于商城的job一次会取一批数据（比如:20条记录），在极短的时间内（其实就是在一个for循环中）多次调用接口，可能存在相同的请求参数连续调用复制商品接口情况。于是，出现了并发插入重复数据的问题。</p><p>为什么会出现这个问题呢？</p><h2 id="_4-多线程消费" tabindex="-1">4. 多线程消费 <a class="header-anchor" href="#_4-多线程消费" aria-label="Permalink to &quot;4. 多线程消费&quot;">​</a></h2><p><code>RocketMQ</code>的消费者，为了性能考虑，默认是用多线程并发消费的，最大支持<code>64</code>个线程。</p><p>例如：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RocketMQMessageListener</span><span style="color:#89DDFF;">(</span><span style="color:#FFCB6B;">topic</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">${com.susan.topic:PRODUCT_TOPIC}</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#FFCB6B;">consumerGroup</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">${com.susan.group:PRODUCT_TOPIC_GROUP}</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Service</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">class</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">MessageReceiver</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">implements</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">RocketMQListener</span><span style="color:#89DDFF;">&lt;</span><span style="color:#C792EA;">MessageExt</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Override</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">onMessage</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">MessageExt</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">message</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> message </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">new</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">String</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">message</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getBody</span><span style="color:#89DDFF;">(),</span><span style="color:#A6ACCD;"> StandardCharsets</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">UTF_8</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#82AAFF;">doSamething</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">message</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>也就是说，如果在极短的时间内，连续发送重复的消息，就会被不同的线程消费。</p><p>即使在代码中有这样的判断：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">Product</span><span style="color:#A6ACCD;"> oldProduct </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">hashCode</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">oldProduct </span><span style="color:#89DDFF;">==</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">null)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    productMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">insert</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">product</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>在插入数据之前，先判断该数据是否已经存在，只有不存在才会插入。</p><p>但由于在并发情况下，不同的线程都判断商品数据不存在，于是同时进行了插入操作，所以就产生了<code>重复数据</code>。</p><p>如下图所示：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072314673.png" alt="image-20220707231409598" style="zoom:50%;"><h2 id="_5-顺序消费" tabindex="-1">5. 顺序消费 <a class="header-anchor" href="#_5-顺序消费" aria-label="Permalink to &quot;5. 顺序消费&quot;">​</a></h2><p>为了解决上述并发消费重复消息的问题，我们从两方面着手：</p><ol><li>商城系统修复产生重复记录的bug。</li><li>基础系统将消息改成<code>单线程顺序消费</code>。</li></ol><p>我仔细思考了一下，如果只靠商城系统修复bug，以后很难避免不出现类似的重复商品问题，比如：如果用户在极短的时间内点击创建商品按钮多次，或者商城系统主动发起重试。</p><p>所以，基础系统还需进一步处理。</p><p>其实<code>RocketMQ</code>本身是支持顺序消费的，需要消息的生产者和消费者一起改。</p><p>生产者改为：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">rocketMQTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">asyncSendOrderly</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">topic</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> message</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> hashKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">new</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">SendCallback</span><span style="color:#89DDFF;">()</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Override</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">onSuccess</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">SendResult</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">sendResult</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">info</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">sendMessage success</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Override</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">onException</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">Throwable</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      log</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">error</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">sendMessage failed!</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">});</span></span></code></pre></div><p>重点是要调用<code>rocketMQTemplate</code>对象的<code>asyncSendOrderly</code>方法，发送顺序消息。</p><p>消费者改为：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">RocketMQMessageListener</span><span style="color:#89DDFF;">(</span><span style="color:#FFCB6B;">topic</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">${com.susan.topic:PRODUCT_TOPIC}</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#FFCB6B;">consumeMode</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> ConsumeMode</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">ORDERLY</span><span style="color:#89DDFF;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#FFCB6B;">consumerGroup</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">${com.susan.group:PRODUCT_TOPIC_GROUP}</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">)</span></span>
<span class="line"><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Service</span></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">class</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">MessageReceiver</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">implements</span><span style="color:#A6ACCD;"> </span><span style="color:#FFCB6B;">RocketMQListener</span><span style="color:#89DDFF;">&lt;</span><span style="color:#C792EA;">MessageExt</span><span style="color:#89DDFF;">&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">@</span><span style="color:#C792EA;">Override</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">onMessage</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">MessageExt</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">message</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> message </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">new</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">String</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">message</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">getBody</span><span style="color:#89DDFF;">(),</span><span style="color:#A6ACCD;"> StandardCharsets</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">UTF_8</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">        </span><span style="color:#82AAFF;">doSamething</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">message</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>接收消息的重点是<code>RocketMQMessageListener</code>注解中的<code>consumeMode</code>参数，要设置成<code>ConsumeMode.ORDERLY</code>，这样就能顺序消费消息了。</p><p>修改后关键流程图如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072316420.png" alt="image-20220707231619354" style="zoom:50%;"><p>两边都修改之后，复制商品这一块就没有再出现重复商品的问题了。</p><p>But，修完bug之后，我又思考了良久。</p><p>复制商品只是创建商品的其中一个入口，如果有其他入口，跟复制商品功能同时创建新商品呢？</p><p>不也会出现重复商品问题？</p><p>虽说，这种概率非常非常小。</p><p>但如果一旦出现重复商品问题，后续涉及到要合并商品的数据，非常麻烦。</p><p>经过这一次的教训，一定要防微杜渐。</p><p>不管是用户，还是自己的内部系统，从不同的入口创建商品，都需要解决重复商品创建问题。</p><p>那么，如何解决这个问题呢？</p><h2 id="_6-唯一索引" tabindex="-1">6. 唯一索引 <a class="header-anchor" href="#_6-唯一索引" aria-label="Permalink to &quot;6. 唯一索引&quot;">​</a></h2><p>解决重复商品数据问题，最快成本最低最有效的办法是：<code>给表建唯一索引</code>。</p><p>想法是好的，但我们这边有个规范就是：<code>业务表必须都是逻辑删除</code>。</p><p>而我们都知道，要删除表的某条记录的话，如果用<code>delete</code>语句操作的话。</p><p>例如：</p><div class="language-sql"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">delete</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">from</span><span style="color:#A6ACCD;"> product </span><span style="color:#F78C6C;">where</span><span style="color:#A6ACCD;"> id</span><span style="color:#89DDFF;">=</span><span style="color:#F78C6C;">123</span><span style="color:#A6ACCD;">;</span></span></code></pre></div><p>这种delete操作是<code>物理删除</code>，即该记录被删除之后，后续通过sql语句基本查不出来。（不过通过其他技术手段可以找回，那是后话了）</p><p>还有另外一种是逻辑删除，主要是通过<code>update</code>语句操作的。</p><p>例如：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">update product set delete_status</span><span style="color:#89DDFF;">=</span><span style="color:#F78C6C;">1</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">edit_time</span><span style="color:#89DDFF;">=</span><span style="color:#82AAFF;">now</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">3</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span></span>
<span class="line"><span style="color:#A6ACCD;">where id</span><span style="color:#89DDFF;">=</span><span style="color:#F78C6C;">123</span><span style="color:#89DDFF;">;</span></span></code></pre></div><p>逻辑删除需要在表中额外增加一个<code>删除状态字段</code>，用于记录数据是否被删除。在所有的业务查询的地方，都需要过滤掉已经删除的数据。</p><p>通过这种方式删除数据之后，数据任然还在表中，只是从逻辑上过滤了删除状态的数据而已。</p><p>其实对于这种逻辑删除的表，是没法加<code>唯一索引</code>的。</p><p>为什么呢？</p><p>假设之前给商品表中的name和model加了唯一索引，如果用户把某条记录删除了，delete_status设置成1了。后来，该用户发现不对，又重新添加了一模一样的商品。</p><p>由于唯一索引的存在，该用户第二次添加商品会失败，即使该商品已经被删除了，也没法再添加了。</p><p>这个问题显然有点严重。</p><p>有人可能会说：把name、model和delete_status三个字段同时做成唯一索引不就行了？</p><p>答：这样做确实可以解决用户逻辑删除了某个商品，后来又重新添加相同的商品时，添加不了的问题。但如果第二次添加的商品，又被删除了。该用户第三次添加相同的商品，不也出现问题了？</p><p>由此可见，如果表中有逻辑删除功能，是不方便创建唯一索引的。</p><h2 id="_5-分布式锁" tabindex="-1">5. 分布式锁 <a class="header-anchor" href="#_5-分布式锁" aria-label="Permalink to &quot;5. 分布式锁&quot;">​</a></h2><p>接下来，你想到的第二种解决数据重复问题的办法可能是：<code>加分布式锁</code>。</p><p>目前最常用的性能最高的分布式锁，可能是<code>redis分布式锁</code>了。</p><p>使用redis分布式锁的伪代码如下：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">String</span><span style="color:#A6ACCD;"> result </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> jedis</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">set</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> requestId</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">NX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">PX</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> expireTime</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">OK</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">equals</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">result</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#82AAFF;">doSamething</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">      </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">false;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">finally</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#82AAFF;">unlock</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">lockKey</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">requestId</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>不过需要在<code>finally</code>代码块中<code>释放锁</code>。</p><p>其中lockKey是由商品表中的name和model组合而成的，requestId是每次请求的唯一标识，以便于它每次都能正确得释放锁。还需要设置一个过期时间expireTime，防止释放锁失败，锁一直存在，导致后面的请求没法获取锁。</p><p>如果只是单个商品，或者少量的商品需要复制添加，则加分布式锁没啥问题。</p><p>主要流程如下：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072315995.png" alt="image-20220707231533925" style="zoom:50%;"><p>可以在复制添加商品之前，先尝试加锁。如果加锁成功，则在查询商品是否存在，如果不存在，则添加商品。此外，在该流程中如果加锁失败，或者查询商品时不存在，则直接返回。</p><p>加分布式锁的目的是：保证查询商品和添加商品的两个操作是原子性的操作。</p><p>但现在的问题是，我们这次需要复制添加的商品数量很多，如果每添加一个商品都要加分布式锁的话，会非常影响性能。</p><p>显然对于批量接口，加redis分布式锁，不是一个理想的方案。</p><h2 id="_6-统一mq异步处理" tabindex="-1">6. 统一mq异步处理 <a class="header-anchor" href="#_6-统一mq异步处理" aria-label="Permalink to &quot;6. 统一mq异步处理&quot;">​</a></h2><p>前面我们已经聊过，在批量复制商品的接口，我们是通过RocketMQ的顺序消息，单线程异步复制添加商品的，可以暂时解决商品重复的问题。</p><p>但那只改了一个添加商品的入口，还有其他添加商品的入口。</p><p>能不能把添加商品的底层逻辑统一一下，最终都调用同一段代码。然后通过RocketMQ的顺序消息，单线程异步添加商品。</p><p>主要流程如下图所示：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207072315125.png" alt="image-20220707231509994" style="zoom:50%;"><p>这样确实能够解决重复商品的问题。</p><p>但同时也带来了另外两个问题：</p><ol><li>现在所有的添加商品功能都改成异步了，之前同步添加商品的接口如何返回数据呢？这就需要修改前端交互，否则会影响用户体验。</li><li>之前不同的添加商品入口，是多线程添加商品的，现在改成只能由一个线程添加商品，这样修改的结果导致添加商品的整体效率降低了。</li></ol><p>由此，综合考虑了一下各方面因素，这个方案最终被否定了。</p><h2 id="_7-insert-on-duplicate-key-update" tabindex="-1">7. insert on duplicate key update <a class="header-anchor" href="#_7-insert-on-duplicate-key-update" aria-label="Permalink to &quot;7. insert on duplicate key update&quot;">​</a></h2><p>其实，在mysql中存在这样的语法，即：<code>insert on duplicate key update</code>。</p><p>在添加数据时，mysql发现数据不存在，则直接<code>insert</code>。如果发现数据已经存在了，则做<code>update</code>操作。</p><p>不过要求表中存在<code>唯一索引</code>或<code>PRIMARY KEY</code>，这样当这两个值相同时，才会触发更新操作，否则是插入。</p><p>现在的问题是PRIMARY KEY是商品表的主键，是根据<code>雪花算法</code>提前生成的，不可能产生重复的数据。</p><p>但由于商品表有逻辑删除功能，导致唯一索引在商品表中创建不了。</p><p>由此，insert on duplicate key update这套方案，暂时也没法用。</p><p>此外，insert on duplicate key update在高并发的情况下，可能会产生<code>死锁</code>问题，需要特别注意一下。</p><p>感兴趣的小伙伴，也可以找我私聊。</p><p>其实insert on duplicate key update的实战，我在另一篇文章《<a href="https://mp.weixin.qq.com/s?__biz=MzkwNjMwMTgzMQ==&amp;mid=2247490289&amp;idx=1&amp;sn=bc311da9f4a4d3f48ee5dc207bf31a8b&amp;chksm=c0ebc219f79c4b0fc711116723b9df3a5531cda32f0f5d00f065910aa552af6ff03b3f1528fc&amp;token=751314179&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">我用kafka两年踩过的一些非比寻常的坑</a>》中介绍过的，感兴趣的小伙伴，可以看看。</p><h2 id="_8-insert-ignore" tabindex="-1">8. insert ignore <a class="header-anchor" href="#_8-insert-ignore" aria-label="Permalink to &quot;8. insert ignore&quot;">​</a></h2><p>在mysql中还存在这样的语法，即：<code>insert ... ignore</code>。</p><p>在insert语句执行的过程中：mysql发现如果数据重复了，就忽略，否则就会插入。</p><p>它主要是用来忽略，插入重复数据产生的<code>Duplicate entry &#39;XXX&#39; for key &#39;XXXX&#39;</code>异常的。</p><p>不过也要求表中存在<code>唯一索引</code>或<code>PRIMARY KEY</code>。</p><p>但由于商品表有逻辑删除功能，导致唯一索引在商品表中创建不了。</p><p>由此可见，这个方案也不行。</p><p>温馨的提醒一下，使用insert ... ignore也有可能会导致<code>死锁</code>。</p><h2 id="_9-防重表" tabindex="-1">9. 防重表 <a class="header-anchor" href="#_9-防重表" aria-label="Permalink to &quot;9. 防重表&quot;">​</a></h2><p>之前聊过，因为有逻辑删除功能，给商品表加唯一索引，行不通。</p><p>后面又说了加分布式锁，或者通过mq单线程异步添加商品，影响创建商品的性能。</p><p>那么，如何解决问题呢？</p><p>我们能否换一种思路，加一张<code>防重表</code>，在防重表中增加商品表的name和model字段作为唯一索引。</p><p>例如：</p><div class="language-sql"><button title="Copy Code" class="copy"></button><span class="lang">sql</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#F78C6C;">CREATE</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">TABLE</span><span style="color:#A6ACCD;"> `</span><span style="color:#82AAFF;">product_unique</span><span style="color:#A6ACCD;">` (</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">id</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">bigint</span><span style="color:#A6ACCD;">(</span><span style="color:#F78C6C;">20</span><span style="color:#A6ACCD;">) </span><span style="color:#F78C6C;">NOT NULL</span><span style="color:#A6ACCD;"> COMMENT </span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">id</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">name</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">varchar</span><span style="color:#A6ACCD;">(</span><span style="color:#F78C6C;">130</span><span style="color:#A6ACCD;">) </span><span style="color:#C792EA;">DEFAULT</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">NULL</span><span style="color:#A6ACCD;"> COMMENT </span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">名称</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">model</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">varchar</span><span style="color:#A6ACCD;">(</span><span style="color:#F78C6C;">255</span><span style="color:#A6ACCD;">)  </span><span style="color:#F78C6C;">NOT NULL</span><span style="color:#A6ACCD;"> COMMENT </span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">规格</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">user_id</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">bigint</span><span style="color:#A6ACCD;">(</span><span style="color:#F78C6C;">20</span><span style="color:#A6ACCD;">) unsigned </span><span style="color:#F78C6C;">NOT NULL</span><span style="color:#A6ACCD;"> COMMENT </span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">创建用户id</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">user_name</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">varchar</span><span style="color:#A6ACCD;">(</span><span style="color:#F78C6C;">30</span><span style="color:#A6ACCD;">)  </span><span style="color:#F78C6C;">NOT NULL</span><span style="color:#A6ACCD;"> COMMENT </span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">创建用户名称</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">create_date</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">datetime</span><span style="color:#A6ACCD;">(</span><span style="color:#F78C6C;">3</span><span style="color:#A6ACCD;">) </span><span style="color:#F78C6C;">NOT NULL</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">DEFAULT</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">CURRENT_TIMESTAMP</span><span style="color:#A6ACCD;">(</span><span style="color:#F78C6C;">3</span><span style="color:#A6ACCD;">) COMMENT </span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">创建时间</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">,</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#C792EA;">PRIMARY KEY</span><span style="color:#A6ACCD;"> (</span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">id</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;">),</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#F78C6C;">UNIQUE</span><span style="color:#A6ACCD;"> </span><span style="color:#F78C6C;">KEY</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">ux_name_model</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;"> (</span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">name</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;">,</span><span style="color:#89DDFF;">`</span><span style="color:#C3E88D;">model</span><span style="color:#89DDFF;">`</span><span style="color:#A6ACCD;">)</span></span>
<span class="line"><span style="color:#A6ACCD;">) ENGINE</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">InnoDB </span><span style="color:#C792EA;">DEFAULT</span><span style="color:#A6ACCD;"> CHARSET</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">utf8mb4 COMMENT</span><span style="color:#89DDFF;">=</span><span style="color:#89DDFF;">&#39;</span><span style="color:#C3E88D;">商品防重表</span><span style="color:#89DDFF;">&#39;</span><span style="color:#A6ACCD;">;</span></span></code></pre></div><p>其中表中的id可以用商品表的id，表中的name和model就是商品表的name和model，不过在这张防重表中增加了这两个字段的唯一索引。</p><p>视野一下子被打开了。</p><p>在添加商品数据之前，先添加防重表。如果添加成功，则说明可以正常添加商品，如果添加失败，则说明有重复数据。</p><p>防重表添加失败，后续的业务处理，要根据实际业务需求而定。</p><p>如果业务上允许添加一批商品时，发现有重复的，直接抛异常，则可以提示用户：系统检测到重复的商品，请刷新页面重试。</p><p>例如：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  transactionTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">execute</span><span style="color:#89DDFF;">((</span><span style="color:#A6ACCD;">status</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">-&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      productUniqueMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">batchInsert</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productUniqueList</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">      productMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">batchInsert</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productList</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> Boolean</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">TRUE</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">});</span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">DuplicateKeyException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">   </span><span style="color:#89DDFF;font-style:italic;">throw</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">new</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">BusinessException</span><span style="color:#89DDFF;">(</span><span style="color:#89DDFF;">&quot;</span><span style="color:#C3E88D;">系统检测到重复的商品，请刷新页面重试</span><span style="color:#89DDFF;">&quot;</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>在批量插入数据时，如果出现了重复数据，捕获<code>DuplicateKeyException</code>异常，转换成<code>BusinessException</code>这样运行时的业务异常。</p><p>还有一种业务场景，要求即使出现了重复的商品，也不抛异常，让业务流程也能够正常走下去。</p><p>例如：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  transactionTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">execute</span><span style="color:#89DDFF;">((</span><span style="color:#A6ACCD;">status</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">-&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      productUniqueMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">insert</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productUnique</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">      productMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">insert</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">product</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> Boolean</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">TRUE</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">});</span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">DuplicateKeyException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">   product </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> productMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">product</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>在插入数据时，如果出现了重复数据，则捕获<code>DuplicateKeyException</code>，在<code>catch</code>代码块中再查询一次商品数据，将数据库已有的商品直接返回。</p><p>如果调用了同步添加商品的接口，这里非常关键的一点，是要返回已有数据的id，业务系统做后续操作，要拿这个id操作。</p><p>当然在执行execute之前，还是需要先查一下商品数据是否存在，如果已经存在，则直接返回已有数据，如果不存在，才执行execute方法。这一步千万不能少。</p><p>例如：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">Product</span><span style="color:#A6ACCD;"> oldProduct </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> productMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">product</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">if</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">Objects</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">nonNull</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">oldProduct</span><span style="color:#89DDFF;">))</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> oldProduct</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">  transactionTemplate</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">execute</span><span style="color:#89DDFF;">((</span><span style="color:#A6ACCD;">status</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">-&gt;</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">      productUniqueMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">insert</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">productUnique</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">      productMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">insert</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">product</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> Boolean</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">TRUE</span><span style="color:#89DDFF;">;</span></span>
<span class="line"><span style="color:#A6ACCD;">  </span><span style="color:#89DDFF;">});</span></span>
<span class="line"><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">DuplicateKeyException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">   product </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> productMapper</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">query</span><span style="color:#89DDFF;">(</span><span style="color:#A6ACCD;">product</span><span style="color:#89DDFF;">);</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;font-style:italic;">return</span><span style="color:#A6ACCD;"> product</span><span style="color:#89DDFF;">;</span></span></code></pre></div><blockquote><p>千万注意：防重表和添加商品的操作必须要在同一个事务中，否则会出问题。</p></blockquote><p>顺便说一下，还需要对商品的删除功能做特殊处理一下，在逻辑删除商品表的同时，要物理删除防重表。用商品表id作为查询条件即可。</p><p>说实话，解决重复数据问题的方案挺多的，没有最好的方案，只有最适合业务场景的，最优的方案。</p><p>此外，如果你对重复数据衍生出的幂等性问题感兴趣的话，可以看看我的另一篇文章《<a href="https://mp.weixin.qq.com/s?__biz=MzkwNjMwMTgzMQ==&amp;mid=2247490307&amp;idx=1&amp;sn=b9eeb427c33cb171da6c3f11243a88f4&amp;chksm=c0ebc3ebf79c4afd0d5a1851a975534b672d86c531d28c5933013140173e794f5f53e78a6765&amp;token=751314179&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">高并发下如何保证接口的幂等性？</a>》，里面有非常详细的介绍。</p><h1 id="面对千万级、亿级流量怎么处理" tabindex="-1">面对千万级、亿级流量怎么处理？ <a class="header-anchor" href="#面对千万级、亿级流量怎么处理" aria-label="Permalink to &quot;面对千万级、亿级流量怎么处理？&quot;">​</a></h1><p>这是一道很常见的面试题，但是大多数人并不知道怎么回答，这种问题其实可以有很多形式的提问方式，你一定见过而且感觉无从下手：</p><p>面对业务急剧增长你怎么处理？</p><p>业务量增长10倍、100倍怎么处理？</p><p>你们系统怎么支撑高并发的？</p><p>怎么设计一个高并发系统？</p><p>高并发系统都有什么特点？</p><p>... ...</p><p>诸如此类，问法很多，但是面试这种类型的问题，看着很难无处下手，但是我们可以有一个常规的思路去回答，就是围绕支撑高并发的业务场景怎么设计系统才合理？如果你能想到这一点，那接下来我们就可以围绕硬件和软件层面怎么支撑高并发这个话题去阐述了。本质上，这个问题就是综合考验你对各个细节是否知道怎么处理，是否有经验处理过而已。</p><p>面对超高的并发，首先硬件层面机器要能扛得住，其次架构设计做好微服务的拆分，代码层面各种缓存、削峰、解耦等等问题要处理好，数据库层面做好读写分离、分库分表，稳定性方面要保证有监控，熔断限流降级该有的必须要有，发生问题能及时发现处理。</p><h2 id="微服务架构演化" tabindex="-1">微服务架构演化 <a class="header-anchor" href="#微服务架构演化" aria-label="Permalink to &quot;微服务架构演化&quot;">​</a></h2><p>在互联网早期的时候，单体架构就足以支撑起日常的业务需求，大家的所有业务服务都在一个项目里，部署在一台物理机器上。</p><p>所有的业务包括你的交易订单、会员、库存、商品、营销等等都夹杂在一起，当流量一旦高起来之后，单体架构的问题就暴露出来了，机器挂了所有的业务全部无法使用了。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201840619.png" alt="image-20220720184025549" style="zoom:50%;"><p>于是，集群架构的架构开始出现，单机无法抗住的压力，最简单的办法就是水平拓展横向扩容。这样，通过负载均衡把压力流量分摊到不同的机器上，暂时是解决了单点导致服务不可用的问题。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201840139.png" alt="image-20220720184045064" style="zoom:50%;"><p>但是随着业务的发展，在一个项目里维护所有的业务场景使开发和代码维护变得越来越困难，一个简单的需求改动都需要发布整个服务，代码的合并冲突也会变得越来越频繁，同时线上故障出现的可能性越大，微服务的架构模式就诞生了。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201841503.png" alt="image-20220720184105440" style="zoom:80%;"><p>把每个独立的业务拆分开独立部署，开发和维护的成本降低，集群能承受的压力也提高了，再也不会出现一个小小的改动点需要牵一发而动全身了。</p><p>以上的点从高并发的角度而言，似乎都可以归类为通过服务拆分和集群物理机器的扩展提高了整体的系统抗压能力，那么，随之拆分而带来的问题也就是高并发系统需要解决的问题。</p><h2 id="通信" tabindex="-1">通信 <a class="header-anchor" href="#通信" aria-label="Permalink to &quot;通信&quot;">​</a></h2><p>微服务化的拆分带来的好处和便利性是显而易见的，但是与此同时各个微服务之间的通信就需要考虑了。</p><p>对于SOA、微服务化的架构而言，就对部署、运维、服务治理、链路追踪等等有了更高的要求。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201841873.png" alt="image-20220720184122795" style="zoom:67%;"><p>基于此，无论选用何种框架Spring Cloud、Spring Cloud Alibaba、Dubbo、Thrift、gRpc其实都一样。</p><p>于现在国内的技术栈选择来说，大厂基本都是自研，中小厂更多采用如Dubbo这类框架，现在来说，Spring Cloud Alibaba应该是未来一段时间的主流方向。</p><p>但是无论使用何种框架，一些基本原理都是应该了解的。此处以Dubbo举例。</p><p><a href="https://mp.weixin.qq.com/s?__biz=MzkzNTEwOTAxMA==&amp;mid=2247485036&amp;idx=1&amp;sn=9765f53de06dbc7a3b8803d7297ca91f&amp;chksm=c2b24e91f5c5c787f8d2b3440e395d6c236c473af03b72593fbe0b66fdecb4ce3635308ee2b3&amp;token=1236837018&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">更多请查看：《我想进大厂》之Dubbo普普通通9问</a></p><h3 id="dubbo工作原理" tabindex="-1">Dubbo工作原理 <a class="header-anchor" href="#dubbo工作原理" aria-label="Permalink to &quot;Dubbo工作原理&quot;">​</a></h3><ol><li>服务启动的时候，provider和consumer根据配置信息，连接到注册中心register，分别向注册中心注册和订阅服务</li><li>register根据服务订阅关系，返回provider信息到consumer，同时consumer会把provider信息缓存到本地。如果信息有变更，consumer会收到来自register的推送</li><li>consumer生成代理对象，同时根据负载均衡策略，选择一台provider，同时定时向monitor记录接口的调用次数和时间信息</li><li>拿到代理对象之后，consumer通过代理对象发起接口调用</li><li>provider收到请求后对数据进行反序列化，然后通过代理调用具体的接口实现</li></ol><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201842719.png" alt="image-20220720184214618" style="zoom:67%;"><h3 id="dubbo负载均衡策略" tabindex="-1">Dubbo负载均衡策略 <a class="header-anchor" href="#dubbo负载均衡策略" aria-label="Permalink to &quot;Dubbo负载均衡策略&quot;">​</a></h3><ol><li>加权随机：假设我们有一组服务器 servers = [A, B, C]，他们对应的权重为 weights = [5, 3, 2]，权重总和为10。现在把这些权重值平铺在一维坐标值上，[0, 5) 区间属于服务器 A，[5, 8) 区间属于服务器 B，[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数，然后计算这个随机数会落到哪个区间上就可以了。</li><li>最小活跃数：每个服务提供者对应一个活跃数 active，初始情况下，所有服务提供者活跃数均为0。每收到一个请求，活跃数加1，完成请求后则将活跃数减1。在服务运行一段时间后，性能好的服务提供者处理请求的速度更快，因此活跃数下降的也越快，此时这样的服务提供者能够优先获取到新的服务请求。</li><li>一致性hash：通过hash算法，把provider的invoke和随机节点生成hash，并将这个 hash 投射到 [0, 2^32 - 1] 的圆环上，查询的时候根据key进行md5然后进行hash，得到第一个节点的值大于等于当前hash的invoker。</li></ol><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201842140.png" alt="image-20220720184228067" style="zoom:67%;"><ol><li>加权轮询：比如服务器 A、B、C 权重比为 5:2:1，那么在8次请求中，服务器 A 将收到其中的5次请求，服务器 B 会收到其中的2次请求，服务器 C 则收到其中的1次请求。</li></ol><h3 id="集群容错" tabindex="-1">集群容错 <a class="header-anchor" href="#集群容错" aria-label="Permalink to &quot;集群容错&quot;">​</a></h3><ol><li>Failover Cluster失败自动切换：dubbo的默认容错方案，当调用失败时自动切换到其他可用的节点，具体的重试次数和间隔时间可用通过引用服务的时候配置，默认重试次数为1也就是只调用一次。</li><li>Failback Cluster失败自动恢复：在调用失败，记录日志和调用信息，然后返回空结果给consumer，并且通过定时任务每隔5秒对失败的调用进行重试</li><li>Failfast Cluster快速失败：只会调用一次，失败后立刻抛出异常</li><li>Failsafe Cluster失败安全：调用出现异常，记录日志不抛出，返回空结果</li><li>Forking Cluster并行调用多个服务提供者：通过线程池创建多个线程，并发调用多个provider，结果保存到阻塞队列，只要有一个provider成功返回了结果，就会立刻返回结果</li><li>Broadcast Cluster广播模式：逐个调用每个provider，如果其中一台报错，在循环调用结束后，抛出异常。</li></ol><h2 id="消息队列" tabindex="-1">消息队列 <a class="header-anchor" href="#消息队列" aria-label="Permalink to &quot;消息队列&quot;">​</a></h2><p>对于MQ的作用大家都应该很了解了，削峰填谷、解耦。依赖消息队列，同步转异步的方式，可以降低微服务之间的耦合。</p><p>对于一些不需要同步执行的接口，可以通过引入消息队列的方式异步执行以提高接口响应时间。</p><p>比如在交易完成之后需要扣库存，然后可能需要给会员发放积分，本质上，发积分的动作应该属于履约服务，对实时性的要求也不高，我们只要保证最终一致性也就是能履约成功就行了。对于这种同类性质的请求就可以走MQ异步，也就提高了系统抗压能力了。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201842525.png" alt="image-20220720184255447" style="zoom:67%;"><p>对于消息队列而言，最大的挑战怎么在使用的时候保证消息的可靠性、不丢失？</p><p><a href="https://mp.weixin.qq.com/s?__biz=MzkzNTEwOTAxMA==&amp;mid=2247484898&amp;idx=1&amp;sn=87c7c4e482d93adc5a49b70ddd4c5097&amp;chksm=c2b24d1ff5c5c40930baf3a1ec358c4ce596ef9ed35129ebddb5d86b45f23b90e6440d644a03&amp;token=1236837018&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">更多请查看：《我想进大厂》之MQ夺命连环11问</a></p><h3 id="消息可靠性" tabindex="-1">消息可靠性 <a class="header-anchor" href="#消息可靠性" aria-label="Permalink to &quot;消息可靠性&quot;">​</a></h3><p>消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。</p><h4 id="生产者丢失" tabindex="-1">生产者丢失 <a class="header-anchor" href="#生产者丢失" aria-label="Permalink to &quot;生产者丢失&quot;">​</a></h4><p>生产者丢失消息的可能点在于程序发送失败抛异常了没有重试处理，或者发送的过程成功但是过程中网络闪断MQ没收到，消息就丢失了。</p><p>由于同步发送的一般不会出现这样使用方式，所以我们就不考虑同步发送的问题，我们基于异步发送的场景来说。</p><p>异步发送分为两个方式：<strong>异步有回调和异步无回调</strong>，无回调的方式，生产者发送完后不管结果可能就会造成消息丢失，而通过异步发送+回调通知+本地消息表的形式我们就可以做出一个解决方案。以下单的场景举例。</p><ol><li>下单后先保存本地数据和MQ消息表，这时候消息的状态是发送中，如果本地事务失败，那么下单失败，事务回滚。</li><li>下单成功，直接返回客户端成功，异步发送MQ消息</li><li>MQ回调通知消息发送结果，对应更新数据库MQ发送状态</li><li>JOB轮询超过一定时间（时间根据业务配置）还未发送成功的消息去重试</li><li>在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息，告警，人工介入。</li></ol><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201939745.png" alt="image-20220720193955630" style="zoom:50%;"><p>一般而言，对于大部分场景来说异步回调的形式就可以了，只有那种需要完全保证不能丢失消息的场景我们做一套完整的解决方案。</p><h4 id="mq丢失" tabindex="-1">MQ丢失 <a class="header-anchor" href="#mq丢失" aria-label="Permalink to &quot;MQ丢失&quot;">​</a></h4><p>如果生产者保证消息发送到MQ，而MQ收到消息后还在内存中，这时候宕机了又没来得及同步给从节点，就有可能导致消息丢失。</p><p>比如RocketMQ：</p><p>RocketMQ分为同步刷盘和异步刷盘两种方式，默认的是异步刷盘，就有可能导致消息还未刷到硬盘上就丢失了，可以通过设置为同步刷盘的方式来保证消息可靠性，这样即使MQ挂了，恢复的时候也可以从磁盘中去恢复消息。</p><p>比如Kafka也可以通过配置做到：</p><div class="language-apl"><button title="Copy Code" class="copy"></button><span class="lang">apl</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#A6ACCD;">acks</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">all 只有参与复制的所有节点全部收到消息，才返回生产者成功。这样的话除非所有的节点都挂了，消息才会丢失。</span></span>
<span class="line"><span style="color:#A6ACCD;">replication</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">factor</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">N</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;">设置大于</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;">的数，这会要求每个partion至少有</span><span style="color:#F78C6C;">2</span><span style="color:#A6ACCD;">个副本</span></span>
<span class="line"><span style="color:#A6ACCD;">min</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">insync</span><span style="color:#89DDFF;">.</span><span style="color:#A6ACCD;">replicas</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">N，设置大于</span><span style="color:#F78C6C;">1</span><span style="color:#A6ACCD;">的数，这会要求leader至少感知到一个follower还保持着连接</span></span>
<span class="line"><span style="color:#A6ACCD;">retries</span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;">N，设置一个非常大的值，让生产者发送失败一直重试</span></span></code></pre></div><p>虽然我们可以通过配置的方式来达到MQ本身高可用的目的，但是都对性能有损耗，怎样配置需要根据业务做出权衡。</p><h4 id="消费者丢失" tabindex="-1">消费者丢失 <a class="header-anchor" href="#消费者丢失" aria-label="Permalink to &quot;消费者丢失&quot;">​</a></h4><p>消费者丢失消息的场景：消费者刚收到消息，此时服务器宕机，MQ认为消费者已经消费，不会重复发送消息，消息丢失。</p><p>RocketMQ默认是需要消费者回复ack确认，而kafka需要手动开启配置关闭自动offset。</p><p>消费方不返回ack确认，重发的机制根据MQ类型的不同发送时间间隔、次数都不尽相同，如果重试超过次数之后会进入死信队列，需要手工来处理了。（Kafka没有这些）</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201940817.png" alt="image-20220720194050701" style="zoom:50%;"><h3 id="消息的最终一致性" tabindex="-1">消息的最终一致性 <a class="header-anchor" href="#消息的最终一致性" aria-label="Permalink to &quot;消息的最终一致性&quot;">​</a></h3><p>事务消息可以达到分布式事务的最终一致性，事务消息就是MQ提供的类似XA的分布式事务能力。</p><p>半事务消息就是MQ收到了生产者的消息，但是没有收到二次确认，不能投递的消息。</p><p>实现原理如下：</p><ol><li>生产者先发送一条半事务消息到MQ</li><li>MQ收到消息后返回ack确认</li><li>生产者开始执行本地事务</li><li>如果事务执行成功发送commit到MQ，失败发送rollback</li><li>如果MQ长时间未收到生产者的二次确认commit或者rollback，MQ对生产者发起消息回查</li><li>生产者查询事务执行最终状态</li><li>根据查询事务状态再次提交二次确认</li></ol><p>最终，如果MQ收到二次确认commit，就可以把消息投递给消费者，反之如果是rollback，消息会保存下来并且在3天后被删除。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201942991.png" alt="image-20220720194224903" style="zoom:67%;"><h2 id="数据库" tabindex="-1">数据库 <a class="header-anchor" href="#数据库" aria-label="Permalink to &quot;数据库&quot;">​</a></h2><p>对于整个系统而言，最终所有的流量的查询和写入都落在数据库上，数据库是支撑系统高并发能力的核心。怎么降低数据库的压力，提升数据库的性能是支撑高并发的基石。主要的方式就是通过读写分离和分库分表来解决这个问题。</p><p>对于整个系统而言，流量应该是一个漏斗的形式。比如我们的日活用户DAU有20万，实际可能每天来到提单页的用户只有3万QPS，最终转化到下单支付成功的QPS只有1万。那么对于系统来说读是大于写的，这时候可以通过读写分离的方式来降低数据库的压力。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201942201.png" alt="image-20220720194245113" style="zoom:50%;"><p>读写分离也就相当于数据库集群的方式降低了单节点的压力。而面对数据的急剧增长，原来的单库单表的存储方式已经无法支撑整个业务的发展，这时候就需要对数据库进行分库分表了。</p><p>针对微服务而言垂直的分库本身已经是做过的，剩下大部分都是分表的方案了。</p><p><a href="https://mp.weixin.qq.com/s?__biz=MzkzNTEwOTAxMA==&amp;mid=2247485546&amp;idx=1&amp;sn=06c7b9c34af217def5f8f147632cd1be&amp;chksm=c2b24097f5c5c9813c9fe6c7e5faec20dd865550c28ed0eba023fff5797103ff946f26fc9b20&amp;token=1236837018&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">更多请查看：百亿级数据分表后怎么分页查询？</a></p><h3 id="水平分表" tabindex="-1">水平分表 <a class="header-anchor" href="#水平分表" aria-label="Permalink to &quot;水平分表&quot;">​</a></h3><p>首先根据业务场景来决定使用什么字段作为分表字段(sharding_key)，比如我们现在日订单1000万，我们大部分的场景来源于C端，我们可以用user_id作为sharding_key，数据查询支持到最近3个月的订单，超过3个月的做归档处理，那么3个月的数据量就是9亿，可以分1024张表，每张表的数据大概就在100万左右。</p><p>比如用户id为100，那我们都经过hash(100)，然后对1024取模，就可以落到对应的表上了。</p><h3 id="分表后的id唯一性" tabindex="-1">分表后的ID唯一性 <a class="header-anchor" href="#分表后的id唯一性" aria-label="Permalink to &quot;分表后的ID唯一性&quot;">​</a></h3><p>因为我们主键默认都是自增的，那么分表之后的主键在不同表就肯定会有冲突了。有几个办法考虑：</p><ol><li>设定步长，比如1-1024张表，每张表初始值不同，设置1024的基础步长，这样主键落到不同的表就不会冲突了。</li><li>分布式ID，自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种</li><li>分表后不使用主键作为查询依据，而是每张表单独新增一个字段作为唯一主键使用，比如订单表订单号是唯一的，不管最终落在哪张表都基于订单号作为查询依据，更新也一样。</li></ol><h3 id="主从同步原理" tabindex="-1">主从同步原理 <a class="header-anchor" href="#主从同步原理" aria-label="Permalink to &quot;主从同步原理&quot;">​</a></h3><ol><li>master提交完事务后，写入binlog</li><li>slave连接到master，获取binlog</li><li>master创建dump线程，推送binglog到slave</li><li>slave启动一个IO线程读取同步过来的master的binlog，记录到relay log中继日志中</li><li>slave再开启一个sql线程读取relay log事件并在slave执行，完成同步</li><li>slave记录自己的binglog</li></ol><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201943878.png" alt="image-20220720194303779" style="zoom:50%;"><p>由于mysql默认的复制方式是异步的，主库把日志发送给从库后不关心从库是否已经处理，这样会产生一个问题就是假设主库挂了，从库处理失败了，这时候从库升为主库后，日志就丢失了。由此产生两个概念。</p><p><strong>全同步复制</strong></p><p>主库写入binlog后强制同步日志到从库，所有的从库都执行完成后才返回给客户端，但是很显然这个方式的话性能会受到严重影响。</p><p><strong>半同步复制</strong></p><p>和全同步不同的是，半同步复制的逻辑是这样，从库写入日志成功后返回ACK确认给主库，主库收到至少一个从库的确认就认为写操作完成。</p><h2 id="缓存" tabindex="-1">缓存 <a class="header-anchor" href="#缓存" aria-label="Permalink to &quot;缓存&quot;">​</a></h2><p>缓存作为高性能的代表，在某些特殊业务可能承担90%以上的热点流量。对于一些活动比如秒杀这种并发QPS可能几十万的场景，引入缓存事先预热可以大幅降低对数据库的压力，10万的QPS对于单机的数据库来说可能就挂了，但是对于如redis这样的缓存来说就完全不是问题。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201943909.png" alt="image-20220720194328820" style="zoom:50%;"><p>以秒杀系统举例，活动预热商品信息可以提前缓存提供查询服务，活动库存数据可以提前缓存，下单流程可以完全走缓存扣减，秒杀结束后再异步写入数据库，数据库承担的压力就小的太多了。当然，引入缓存之后就还要考虑缓存击穿、雪崩、热点一系列的问题了。</p><p><a href="https://mp.weixin.qq.com/s?__biz=MzkzNTEwOTAxMA==&amp;mid=2247484942&amp;idx=1&amp;sn=d5fdb1bee3bc01367f3ffa1db96cfc7f&amp;chksm=c2b24ef3f5c5c7e50079bb994f35c2b858fb88ab754eb80f9be970e935c957291231bc8c7f16&amp;token=1236837018&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">更多请查看：《我想进大厂》之Redis夺命连环11问</a></p><h3 id="热key问题" tabindex="-1">热key问题 <a class="header-anchor" href="#热key问题" aria-label="Permalink to &quot;热key问题&quot;">​</a></h3><p>所谓热key问题就是，突然有几十万的请求去访问redis上的某个特定key，那么这样会造成流量过于集中，达到物理网卡上限，从而导致这台redis的服务器宕机引发雪崩。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201944277.png" alt="image-20220720194410199" style="zoom:50%;"><p>针对热key的解决方案：</p><ol><li>提前把热key打散到不同的服务器，降低压力</li><li>加入二级缓存，提前加载热key数据到内存中，如果redis宕机，走内存查询</li></ol><h3 id="缓存击穿-1" tabindex="-1">缓存击穿 <a class="header-anchor" href="#缓存击穿-1" aria-label="Permalink to &quot;缓存击穿&quot;">​</a></h3><p>缓存击穿的概念就是单个key并发访问过高，过期时导致所有请求直接打到db上，这个和热key的问题比较类似，只是说的点在于过期导致请求全部打到DB上而已。</p><p>解决方案：</p><ol><li>加锁更新，比如请求查询A，发现缓存中没有，对A这个key加锁，同时去数据库查询数据，写入缓存，再返回给用户，这样后面的请求就可以从缓存中拿到数据了。</li><li>将过期时间组合写在value中，通过异步的方式不断的刷新过期时间，防止此类现象。</li></ol><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201944867.png" alt="image-20220720194457776" style="zoom:50%;"><h3 id="缓存穿透-1" tabindex="-1">缓存穿透 <a class="header-anchor" href="#缓存穿透-1" aria-label="Permalink to &quot;缓存穿透&quot;">​</a></h3><p>缓存穿透是指查询不存在缓存中的数据，每次请求都会打到DB，就像缓存不存在一样。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201945297.png" alt="image-20220720194513214" style="zoom:50%;"><p>针对这个问题，加一层布隆过滤器。布隆过滤器的原理是在你存入数据的时候，会通过散列函数将它映射为一个位数组中的K个点，同时把他们置为1。</p><p>这样当用户再次来查询A，而A在布隆过滤器值为0，直接返回，就不会产生击穿请求打到DB了。</p><p>显然，使用布隆过滤器之后会有一个问题就是误判，因为它本身是一个数组，可能会有多个值落到同一个位置，那么理论上来说只要我们的数组长度够长，误判的概率就会越低，这种问题就根据实际情况来就好了。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201945216.png" alt="image-20220720194541131" style="zoom:50%;"><h3 id="缓存雪崩-1" tabindex="-1">缓存雪崩 <a class="header-anchor" href="#缓存雪崩-1" aria-label="Permalink to &quot;缓存雪崩&quot;">​</a></h3><p>当某一时刻发生大规模的缓存失效的情况，比如你的缓存服务宕机了，会有大量的请求进来直接打到DB上，这样可能导致整个系统的崩溃，称为雪崩。雪崩和击穿、热key的问题不太一样的是，他是指大规模的缓存都过期失效了。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201945087.png" alt="image-20220720194555998" style="zoom:50%;"><p>针对雪崩几个解决方案：</p><ol><li>针对不同key设置不同的过期时间，避免同时过期</li><li>限流，如果redis宕机，可以限流，避免同时刻大量请求打崩DB</li><li>二级缓存，同热key的方案。</li></ol><h2 id="稳定性" tabindex="-1">稳定性 <a class="header-anchor" href="#稳定性" aria-label="Permalink to &quot;稳定性&quot;">​</a></h2><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.6.30/202207201946401.png" alt="image-20220720194612314" style="zoom:67%;"><h3 id="熔断" tabindex="-1">熔断 <a class="header-anchor" href="#熔断" aria-label="Permalink to &quot;熔断&quot;">​</a></h3><p>比如营销服务挂了或者接口大量超时的异常情况，不能影响下单的主链路，涉及到积分的扣减一些操作可以在事后做补救。</p><h3 id="限流" tabindex="-1">限流 <a class="header-anchor" href="#限流" aria-label="Permalink to &quot;限流&quot;">​</a></h3><p>对突发如大促秒杀类的高并发，如果一些接口不做限流处理，可能直接就把服务打挂了，针对每个接口的压测性能的评估做出合适的限流尤为重要。</p><h3 id="降级" tabindex="-1">降级 <a class="header-anchor" href="#降级" aria-label="Permalink to &quot;降级&quot;">​</a></h3><p>熔断之后实际上可以说就是降级的一种，以熔断的举例来说营销接口熔断之后降级方案就是短时间内不再调用营销的服务，等到营销恢复之后再调用。</p><h3 id="预案" tabindex="-1">预案 <a class="header-anchor" href="#预案" aria-label="Permalink to &quot;预案&quot;">​</a></h3><p>一般来说，就算是有统一配置中心，在业务的高峰期也是不允许做出任何的变更的，但是通过配置合理的预案可以在紧急的时候做一些修改。</p><h3 id="核对" tabindex="-1">核对 <a class="header-anchor" href="#核对" aria-label="Permalink to &quot;核对&quot;">​</a></h3><p>针对各种分布式系统产生的分布式事务一致性或者受到攻击导致的数据异常，非常需要核对平台来做最后的兜底的数据验证。比如下游支付系统和订单系统的金额做核对是否正确，如果收到中间人攻击落库的数据是否保证正确性。</p><h2 id="总结-1" tabindex="-1">总结 <a class="header-anchor" href="#总结-1" aria-label="Permalink to &quot;总结&quot;">​</a></h2><p>其实可以看到，怎么设计高并发系统这个问题本身他是不难的，无非是基于你知道的知识点，从物理硬件层面到软件的架构、代码层面的优化，使用什么中间件来不断提高系统的抗压能力。</p><p>但是这个问题本身会带来更多的问题，微服务本身的拆分带来了分布式事务的问题，HTTP、RPC框架的使用带来了服务治理、服务发现、路由、集群容错的问题；MQ的引入带来了消息丢失、积压、事务消息、顺序消息的问题；缓存的引入又会带来一致性、雪崩、击穿的问题；数据库的读写分离、分库分表又会带来主从同步延迟、分布式ID、事务一致性的问题。而为了解决这些问题我们又要不断的加入各种措施熔断、限流、降级、离线核对、预案处理等等来防止和追溯这些问题。</p><h1 id="开发一个不崩溃的核酸系统难吗" tabindex="-1">开发一个不崩溃的核酸系统难吗 <a class="header-anchor" href="#开发一个不崩溃的核酸系统难吗" aria-label="Permalink to &quot;开发一个不崩溃的核酸系统难吗&quot;">​</a></h1><p>成都核酸检测系统“崩溃”事件，将东软推至风口浪尖，同时也在技术圈内引发了广泛的讨论。</p><p><strong>开发一个不崩溃的核酸系统到底难不难</strong> ？</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZeap6iaR5wp7wCCpc0LmexvtfsJvOpz03WyAmsVkvwtgogm3hibLFp7JCw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>这篇文章，勇哥想象自己是核酸系统架构师，谈谈自己对核酸系统的理解。</p><h2 id="_1-明确系统边界" tabindex="-1">1 明确系统边界 <a class="header-anchor" href="#_1-明确系统边界" aria-label="Permalink to &quot;1 明确系统边界&quot;">​</a></h2><p>作为架构师，首先需要<strong>明确系统边界</strong>。</p><p>核酸检测核心流程：</p><blockquote><ol><li>医护人员打开核酸系统的手机端应用，录入试管编码 ;</li><li>医护人员扫描居民的健康码；</li><li>医护人员采集咽拭子标本 ；</li><li>检测结束之后，医护人员将检测标本送至检测中心；</li><li>检测中心将检测结果提交到核酸系统，然后核酸系统会将核酸结果同步到健康码系统。</li></ol></blockquote><p>成都核酸系统崩溃时，流程阻塞在步骤一和二。</p><p>本文里我们提到的核酸系统，也就是指<strong>医护人员使用的系统</strong>。而核酸检测系统会将检测结果同步到健康码系统 , 健康码系统面向的是大众居民 , 是高频场景。</p><p>对于成都市居民来讲，与他们关系最为密切的就是两套系统。</p><blockquote><ol><li><strong>核酸系统</strong>：核酸医护人员使用 , 东软负责开发和维护；</li><li><strong>天府健康通</strong>：广大市民使用，腾讯研发和维护。</li></ol></blockquote><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.10.30/202210151747343.png" alt="image-20221015174736242" style="zoom:67%;"><h2 id="_2-崩溃疑云" tabindex="-1">2 崩溃疑云 <a class="header-anchor" href="#_2-崩溃疑云" aria-label="Permalink to &quot;2 崩溃疑云&quot;">​</a></h2><p>核酸系统软件是属于政府购买 (TO G)，市民使用 (TO C) 。</p><p><strong>核酸系统是一个多方协作的系统，它不仅直接和政府有关系，还涉及到多个厂商，一个系统工程背后，除了系统集成商之外，包括多个分包商</strong>。比如西安的一码通，曾集结了电信、东软、美林和安恒等公司。</p><p>正因为这套系统涉及面之广，当成都核酸系统崩溃时，我们需要冷静下来，缕清条理。</p><p>我们先从<strong>基础设施层</strong>的维度来分析，很多互联网公司会将自己的服务部署在阿里云或者腾讯云，部署方便，也可以动态扩容。</p><p>那么核酸系统部署在哪里呢？假如核酸系统是以 SAAS 形态部署（东软自建机房，或者东软采用阿里云/腾讯云服务），那么成都核酸崩溃事件，东软必然脱不了干系 。但东软随后硬气的发了公告：</p><blockquote><p>系统上线后，发现有响应延迟、卡顿等现象，东软集团第一时间组织专家组和坚守现场的公司技术人员，与成都市相关部门一起，排查事故原因，强化安全防护，保证系统运行。**据技术专家研判，目前出现的系统响应延迟、卡顿等现象与核酸检测系统软件无关。**9月3日零点左右，在进行网络调整之后，系统运行平稳顺畅，效率得到极大提升，当日共完成1200万样本采集量。</p></blockquote><p>假如核酸系统没有问题，会不会是网络问题呢？</p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/V71JNV78n29EI0OIsdTEqc7oeASOrXZeDJKBXCgYs3g8Q8IuRNoo92W6IZk8rd4E7Q0gGUmv2asEVNIpvScBIQ/640?wx_fmt=jpeg&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片" style="zoom:67%;"><p>成都核酸系统奔溃时，医护人员以为是信号问题，纷纷举起手中的手机，捕捉信号，而排队的市民却可以刷抖音，头条。</p><p>9月3日下午4点32分，<strong>四川省通信管理局</strong>发文称，“全市通信网络运行平稳，各核酸检测点移动网络覆盖良好，没有出现网络拥塞和故障。”</p><p>我们基本可以做出判断：<strong>成都核酸系统部署在政务云</strong>，也就是政府部门提供基础设施 ，应用开发商将软件部署在政务云机房里 。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.10.30/202210151748580.png" alt="image-20221015174846529" style="zoom:80%;"><p>核算系统崩溃的可能原因：</p><ol><li><p><strong>政务云机房问题</strong></p><p>网络问题（负载均衡，带宽，防火墙）, 或者机房服务器出现故障；</p></li><li><p><strong>核酸系统软件问题</strong></p><p>核酸检测软件确实承载能力有限，软件崩溃了。</p></li></ol><h2 id="_3-应用层设计" tabindex="-1">3 应用层设计 <a class="header-anchor" href="#_3-应用层设计" aria-label="Permalink to &quot;3  应用层设计&quot;">​</a></h2><p>核酸系统是属于高并发应用吗？这里我们做个估算：</p><ul><li><strong>人口估算法</strong>：</li></ul><blockquote><p>据统计成都市人口2千万多人，假设集中在6小时内做核酸，平均每小时支持的并发人数是3531666。每秒支持的并发约为1000。基于检测人员的集中度不均衡的因素，假设高峰期是平均并发的2-3倍。则每秒并发“核酸登记”2000-3000左右。</p></blockquote><ul><li><strong>检测点估算法</strong>：</li></ul><blockquote><p>今年5月份，上海抗疫期间一共有 15000 + 核酸检测点 ，我们假设成都有和上海一样多的核酸检测点。市民在排队核酸检测时，核酸医护人员扫居民健康码的时间间隔在10秒到15秒之间，每个核酸检测点并行两排检测通道，那么每秒并发“核酸登记”也是在 2000-3000 左右。</p></blockquote><p>通过两种估算方法，我们发现：<strong>核酸系统的请求并发度并不高</strong>。</p><p>虽然并发度不高，但每天的业务数据条数量级较高 ，按照东软的公告，每天可以完成1200万核酸样本采集。</p><p>假设核酸检测记录一天1000万条数据，一周就有7000万条，1个月就能达到3亿条数据。那么势必要使用<strong>分库分表</strong>。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.10.30/202210151750201.png" alt="image-20221015175054148" style="zoom:80%;"><blockquote><ol><li>医护人员扫市民的健康码 ，核酸登记的请求发送到 api 网关 , api 网关将请求转发到核酸系统；</li><li>缓存存储检测点，检测批次等基础信息，核酸系统通过缓存判断业务请求是否合法，若合法，则组装真正的入库的数据；</li><li>核酸系统调用分库分表中间件将数据插入到数据库 。</li></ol></blockquote><p>看起来，核酸系统的架构设计还是比较简单清晰的，核心点在于用<strong>分库分表硬挡高流量访问</strong>。</p><p>但现在这种模式就完美了吗 ?</p><p>我们举湖北鄂通码举例，核酸登记后，健康码在 10~20 分钟状态会修改成绿色并标识成：<strong>核酸已检测</strong>，也就是核酸已检测的状态会异步同步到健康码服务。</p><p>我们不由得想到了消息队列 MQ ，MQ 最大的优势在于：<strong>异步</strong>和<strong>解耦</strong>，MQ 模式还有一个优点：当流量激增时，消息队列还可以起到<strong>消峰</strong>的作用。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.10.30/202210151751748.png" alt="image-20221015175136686" style="zoom:80%;"><p>MQ 方案里，核心流程如下：</p><blockquote><ol><li>医护人员扫市民的健康码 ，核酸登记的请求发送到 api 网关 ，api 网关将请求转发到核酸系统；</li><li>缓存存储检测点，检测批次等基础信息，核酸系统通过缓存判断业务请求是否合法，若合法，则组装真正的入库的数据；</li><li>核酸系统将检测记录发送到消息队列，返回给前端响应成功；</li><li>消费者接收消息后调用分库分表中间件将数据插入到数据库 ；</li><li>消费者接收消息后同步状态到健康码服务。</li></ol></blockquote><p>在架构设计中，并不是引入了组件就完事了，更需要考虑如何精准的使用组件。</p><p>比如，使用消息队列 kafka ，如何保证不丢消息，如何保证高可用。使用了分库分表中间件，是不是需要考虑数据异构，以及冷热分离等。</p><h2 id="_4-监控平台" tabindex="-1">4 监控平台 <a class="header-anchor" href="#_4-监控平台" aria-label="Permalink to &quot;4 监控平台&quot;">​</a></h2><p>我们经常讲：研发人员有两只眼睛，一只是监控平台，另一只是日志平台。</p><p>在对性能和高可用讲究的场景里，监控平台的重要性再怎么强调也不过分。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZey8mqnWp0OmHjKTBkRFvh6OnxVibmvYcORAb0ov47ULJ5PLrldHFia4xw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p><strong>▍一、基础运维监控</strong></p><p>基础运维监控负责监控服务器的 CPU、网络、磁盘、负载、网络流量、TCP 连接等指标，并且通过设定报警阈值实时通知指定负责人。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZe2b5djfxh3Pbd5ulp2unLgFrSENaFcGJIa2RunUB2GfZP30IicWyHkGg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片">基础运维监控</p><p>我们在<strong>基础设施层</strong>这一节里提到：</p><p>核酸系统崩溃时，成都政务云不能提供畅通的核酸检测服务 , 可能原因之一是<strong>政务云机房问题</strong>。</p><p>当政务云机房出现问题时，基础运维监控可以帮助运维人员更快的发现问题，并制定解决策略。</p><p><strong>▍二、应用系统监控</strong></p><p>应用系统监控是研发人员接触最多的一种监控类型，系统出现瓶颈的时候，应用系统监控会有最直观的体现。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZetPTP116gKELAYxibVvLovzZ2LZg791nL84NB6ZH2QdpKwibe0t2RAicMw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>笔者一般会关注性能监控，方法可用性监控，方法调用次数监控，JVM 监控这四大类。</p><ul><li>性能监控</li></ul><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZe1l0icofDXuZdU8ib6jQAStiboKXialOv2UXIMGiaNptQIO7Mu18iaUZUVOlw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片">性能监控</p><p>性能监控不同时间段性能分布，实时统计 TP99、TP999 、AVG 、MAX 等维度指标，这也是性能调优的重点关注对象。</p><ul><li><p>方法调用次数监控</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZeia89bZ63xic4IWSnnRTwtFbCUsOXNOub5Dv1bic59YxqhOBpMicnUBIYMw/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p></li></ul><p>方法调用次数监控可以按照机器，时间段分析接口或者方法的调用次数，当大流量来袭时，可以清晰的看到请求的波动。</p><ul><li>方法可用性监控</li></ul><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZepgicFOmgdicNcYfclF9DVJiadtFS8NAUTwv5Sqof2dEiahpDJ6zp8sY2IQ/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片">方法可用率监控</p><p>方法可用性监控是指：当接口被调用或者方法被执行，可能返回异常或者方法执行抛异常，分析该方法是否调用正常，当系统出现严重问题时，方法可用率是一个重要的参考指标。</p><ul><li>JVM 监控</li></ul><p><img src="https://mmbiz.qpic.cn/mmbiz_png/V71JNV78n29EI0OIsdTEqc7oeASOrXZeRxZaibsliaOTUBI32acIGU1TIIovn7DibSwV08OaImQoIE5phrEdDtXJg/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片">JVM 监控</p><p>JVM 监控是 JAVA 工程师特别关注的监控类型，我们会重点关注：堆内存，GC 频率 ，线程数等等。</p><p><strong>▍三、业务监控</strong></p><p>业务监控功能是从业务角度出发，各个应用系统需要从业务层面进行哪些监控，以及提供怎样的业务层面的监控功能支持业务相关的应用系统。</p><p>具体就是对业务数据，业务功能进行监控，实时收集业务流程的数据，并根据设置的策略对业务流程中不符合预期的部分进行预警和报警，并对收集到业务监控数据进行集中统一的存储和各种方式进行展示。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/V71JNV78n29EI0OIsdTEqc7oeASOrXZeVmIrIPddib6Q0TUct6niclK8Gqtk8EMhmRvN8svnloqgd5sg3hLvORpA/640?wx_fmt=jpeg&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>比如订单系统中有一个定时结算的服务，每两分钟执行一次。我们可以在定时任务 JOB 中添加埋点，并配置业务监控，假如十分钟该定时任务没有执行，则发送邮件，短信给相关负责人。</p><h2 id="_5-多方协作" tabindex="-1">5 多方协作 <a class="header-anchor" href="#_5-多方协作" aria-label="Permalink to &quot;5 多方协作&quot;">​</a></h2><p>很多同学都指责东软失职：“核酸系统在仓促上线之后，到底有没有进行完备的性能测试 ”。</p><p>确实，性能测试非常重要 ，通过压测可以知道系统的极限值是多大，当系统承受不住访问时，就会暴露出瓶颈，如服务器 CPU、数据库、内存、响应速度等，从而促使研发团队进行再优化。</p><p>这里我们先按捺指责的冲动，<strong>核酸系统是一个多方协作的系统，它不仅直接和政府有关系，还涉及到多个厂商，一个系统工程背后，除了系统集成商之外，包括多个分包商</strong>。</p><p>《核酸检测系统崩溃，东软该不该背锅？》这篇文章提到：</p><blockquote><p>原则上，监督管理部门要把所有厂商叫在一块协同作战。但没有顶层统筹的强压之下，厂商之间的沟通和协调很难达成。大多数情况之下的压测，各个厂商有点“各自为政”的意思。一般，软件厂商会自己测试自己，鲜少几家联合起来测验。“不同厂商坐在一起的时候，大家都觉得自己没有问题，都会觉得是别人的问题。理由也会一致，我们的系统在别的地方跑过，没出岔子&quot;。甚至应对这一局面，各家的心思都极为微妙。“每个厂家在系统上的投入都是一笔不菲的开支，在应急状态之下，如果上面领导没表态，也没明确是公益性质还是有偿的付出，厂家相应选择也是谨慎的。” 因此大多数情况之下的压测，各个厂商有点“各自为政”的意思。一般，软件厂商会自己测试自己，鲜少几家联合起来压测。</p></blockquote><p>这篇文章的一个观点，“<strong>这是技术层面之外，一个城市应急预案的管理能力问题</strong>。” 我深以为然。</p><h2 id="_6-总结" tabindex="-1">6 总结 <a class="header-anchor" href="#_6-总结" aria-label="Permalink to &quot;6 总结&quot;">​</a></h2><p>假如我是核酸系统的架构师。。。。</p><blockquote><ol><li>我会使用消息队列 + 分库分表来最大程度提升系统的吞吐量。</li><li>我会在使用消息队列中间件的时候，重点关注如何不丢失消息，消息系统如何做到高可用。</li><li>我会使用分库分表中间件时，重点关注冷热分离，如何将数据异构到数据仓库。</li><li>我会在政务云部署监控系统，提供基础运维监控，应用系统监控，业务监控的能力，当系统出现问题时，团队可以以最快的速度发现问题，并解决问题。</li></ol></blockquote><p><strong>可是</strong>，核酸系统是一个多方协作的系统，我们不仅需要和政府沟通，也需要和众多三方厂商协作。</p><p>也许，当我提出需要更多服务器预算时，政府部门的预算并不充足，或者就算充足了，走流程也要一个月的时间；</p><p>也许，当我提出需要部署监控系统，公司会以人力不足为由或者政务云硬件资源不足，否定我的方案；</p><p>也许，当我联调时发现一个三方接口速度慢，排查起来（沟通成本）需要 4-7 天时，我也不得不沉浸在琐事中；</p><p>直到最后，当系统崩溃时，我也只能叹息到：“<strong>尊重技术，尊重专业</strong>”。</p><h1 id="设计了一个支撑-数亿用户的系统⭐" tabindex="-1">设计了一个支撑 数亿用户的系统⭐ <a class="header-anchor" href="#设计了一个支撑-数亿用户的系统⭐" aria-label="Permalink to &quot;设计了一个支撑 数亿用户的系统⭐&quot;">​</a></h1><p>要设计出一套能支撑几十亿人的系统是很困难的。对于软件架构师来说，这一直是一项很大的挑战，但是，从现在开始，看完我的文章，你就会觉得容易很多了。</p><p>下面是我在本文中提到的几个话题：</p><ul><li>从最简单的开始：万事合一。</li><li>可扩展性的艺术：纵向扩展，横向扩展。</li><li>扩展关系型数据库：主 - 从复制、主 - 主复制、联合、分片、非规范化和 SQL 调优。</li><li>使用哪种数据库：NoSQL 还是 SQL？</li><li>先进概念：缓存、CDN、geoDNS 等。</li></ul><blockquote><p>在这篇文章里，我不打算谈论诸如容错、可靠性、高可用性等高性能计算的通用术语。</p></blockquote><p>废话不多说，言归正传。</p><h2 id="_1-从头开始" tabindex="-1">1 从头开始 <a class="header-anchor" href="#_1-从头开始" aria-label="Permalink to &quot;1 从头开始&quot;">​</a></h2><p>在下图中，我要先设计一个有一些用户的基本应用。最容易的方式是在一台服务器上部署整个应用。我们中的大部分人可能都是这样开始的。</p><h3 id="基础设计" tabindex="-1">基础设计 <a class="header-anchor" href="#基础设计" aria-label="Permalink to &quot;基础设计&quot;">​</a></h3><ul><li>一个网站（包括 API）在 Apache（或 Tomcat）等网络服务器上运行。</li><li>一个 Oracle（或 MySQL）之类的数据库。</li></ul><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141041556.png" alt="image-20221114104122466" style="zoom:80%;"><p>我们在同一台物理机上同时拥有 Web 服务器和数据库服务器。</p><p>但是，当前的架构存在下列缺陷：</p><ul><li>如果数据库出现故障，则系统将失效。</li><li>一旦网络服务器出现故障，则会导致整个系统的瘫痪。</li></ul><p>在这种情况下，我们没有故障转移和冗余。如果一个服务器出现故障，所有的都将会失效。</p><h3 id="dns-服务器解析主机名和-ip-地址" tabindex="-1">DNS 服务器解析主机名和 IP 地址 <a class="header-anchor" href="#dns-服务器解析主机名和-ip-地址" aria-label="Permalink to &quot;DNS 服务器解析主机名和 IP 地址&quot;">​</a></h3><p>在上图中，用户（或客户端）连接到 DNS 系统，以获得我们系统所在的服务器的互联网协议（IP）地址。一旦获得 IP 地址，请求就会直接发送到我们的系统。</p><blockquote><p>每次访问网站时，计算机都会执行 DNS 查询。</p></blockquote><p>通常情况下，域名系统（DNS）服务器是作为托管公司提供的付费服务使用的，并不在你自己的服务器上运行。</p><h2 id="_2-可扩展性的艺术" tabindex="-1">2 可扩展性的艺术 <a class="header-anchor" href="#_2-可扩展性的艺术" aria-label="Permalink to &quot;2 可扩展性的艺术&quot;">​</a></h2><p>由于很多原因，我们的系统可能需要进行扩展，如数据量的增加、工作量的增加（如事务的数目），以及用户的增加</p><blockquote><p>可扩展性一般是指添加更多的资源，在不影响用户体验的情况下处理更多的用户、客户机、数据、事务或请求。</p></blockquote><p>我们必须决定怎样才能扩大这个系统的规模。在这种情况下，有以下两种类型的扩展：<code>纵向扩展</code>（scale up） 和<code>横向扩展</code>（scale out）。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141041993.png" alt="image-20221114104153898" style="zoom:67%;"><h3 id="纵向扩展-vs-横向扩展" tabindex="-1">纵向扩展 vs 横向扩展 <a class="header-anchor" href="#纵向扩展-vs-横向扩展" aria-label="Permalink to &quot;纵向扩展 vs 横向扩展&quot;">​</a></h3><p>纵向扩展：<strong>在现有服务器上增加更多的内存和</strong> CPU</p><p>这也被称为“垂直扩展”，是指为了提高系统处理日益增长的负载的能力而使系统能够最大限度地利用资源——例如，通过增加内存和 CPU 来增加服务器的能力。</p><blockquote><p>如果我们运行的服务器有 8G 的内存，那么只要更换或者增加硬件，就可以轻松地提升到 32G，甚至 128G。</p></blockquote><p>有很多方法可以进行纵向扩展，具体如下：</p><ul><li>通过在 RAID 阵列中增加更多的硬盘来增加 I/O 容量。</li><li>通过切换到固态硬盘（SSD）来改善 I/O 访问时间。</li><li>切换到具有更多处理器的服务器。</li><li>通过升级网络接口或安装额外的网络接口来提高网络吞吐量。</li><li>通过增加内存来减少 I/O 操作。</li></ul><p>对于小型系统来说，纵向扩展是一个很好的选择，可以负担得起硬件升级，但也存在一些严重的限制，具体如下：</p><ul><li>“不可能在一台服务器上增加无限的能力”。这主要取决于操作系统和服务器的内存总线宽度。</li><li>给系统升级内存时，必须关掉服务器，因此，如果系统只有一台服务器，停机是不可避免的。</li><li>强大的机器往往要比流行的硬件昂贵很多。</li></ul><p>纵向扩展不仅适用于硬件方面，也适用于软件方面，例如，它包括优化查询和应用程序代码。</p><blockquote><p>相比之下，纵向减缩（scale down）是指从现有的服务器中移除现有的资源，如 CPU、内存和磁盘。</p></blockquote><h3 id="您需要多台服务器吗" tabindex="-1">您需要多台服务器吗 <a class="header-anchor" href="#您需要多台服务器吗" aria-label="Permalink to &quot;您需要多台服务器吗&quot;">​</a></h3><p>当用户数量不断增加时，一台服务器将无法满足需求。我们需要考虑将一台单独的服务器分离到多台服务器上。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141042079.png" alt="image-20221114104224016" style="zoom:67%;"><p>当用户数量不断增加时，一台服务器将无法满足需求。</p><p>采用该架构有如下优势：</p><ul><li>可对 Web 服务器进行不同于数据库服务器的调优。</li><li>网络服务器需要更好的 CPU，而数据库服务器需要更多的内存。</li><li>为 Web 层和数据层提供单独的服务器，允许它们彼此独立地进行扩展。</li></ul><h3 id="横向扩展-添加任意数量的硬件和软件实体" tabindex="-1">横向扩展：添加任意数量的硬件和软件实体 <a class="header-anchor" href="#横向扩展-添加任意数量的硬件和软件实体" aria-label="Permalink to &quot;横向扩展：添加任意数量的硬件和软件实体&quot;">​</a></h3><p>这也被称为“水平扩展”，是指向资源池中添加更多的实体（如机器、服务等）。横向扩展要比纵向扩展更难实现，因为我们必须在建立一个系统之前就把这个问题考虑进去。</p><p>开始时，为了满足最基本的需求，我们需要更多的服务器，因此横向扩展最初往往花费更多，但是到了最后，我们将获得更多的收益。我们需要权衡利弊。</p><p>服务器数量的增长意味着更多的资源需要维护。同时，还必须对系统代码进行修改，以便实现在多台服务器间进行并行和分配工作。</p><blockquote><p>与此相反，横向减缩（Scale in）指的是删除现有服务器的过程。</p></blockquote><h2 id="_3-使用负载均衡器来均衡所有节点上的流量" tabindex="-1">3 使用负载均衡器来均衡所有节点上的流量 <a class="header-anchor" href="#_3-使用负载均衡器来均衡所有节点上的流量" aria-label="Permalink to &quot;3 使用负载均衡器来均衡所有节点上的流量&quot;">​</a></h2><p>负载均衡器是一种专门的硬件或软件组件，它可以帮助分散流量到服务器集群，从而改善系统的响应能力和可用性，包括但不限于应用程序、网站或数据库。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141042602.png" alt="image-20221114104258524" style="zoom:67%;"><p>使用负载均衡器来均衡所有节点之间的流量。</p><p>负载均衡器一般都是在客户端与服务器之间，接受传入的网络及应用程序的流量，并利用各种算法，将流量分配到多个后端服务器。所以，它也可以用于各种场合，比如 Web 服务器与数据库服务器之间，以及客户端和 Web 服务器之间。</p><blockquote><p>HAProxy 和 Nginx 是目前比较受欢迎的开源负载均衡软件。</p></blockquote><p>负载均衡器技术是一种能够改善系统可用性的容错保护方法，具体如下：</p><ul><li>如果服务器 1 脱机，则所有的流量将被路由到服务器 2 和服务器 3。网站就不会脱机。你还需要在服务器池中添加一个新的健康服务器来均衡负载。</li><li>当流量快速增长时，你只需要向网站服务器池添加更多的服务器，负载均衡器将为你路由流量。</li></ul><p>负载均衡器通过不同的策略和任务分配算法对负载进行了最优分配，具体如下：</p><ul><li><strong>循环</strong> ：在这种情况下，每个服务器按顺序接收请求，类似于先进先出（FIFO）。</li><li><strong>最少的连接数</strong> ：连接数最少的服务器将被引导到请求。</li><li><strong>最快的响应时间</strong> ：具有最快响应时间的服务器（最近或经常）将被引导到请求。</li><li><strong>加权</strong> ：较强大的服务器将比较弱的服务器收到更多的请求加权策略。</li><li><strong>IP 哈希</strong> ：在这种情况下，计算客户的 IP 地址的哈希值，将请求重定向到服务器。</li></ul><p>在多个服务器之间均衡请求的最直接方法是使用一个硬件设备。</p><ul><li>从共享 IP 中添加和删除真正的服务器，将会立即发生。</li><li>负载均衡可以根据需要进行。</li></ul><p>软件负载均衡是硬件负载均衡器的一个廉价替代品。其操作于第 4 层（网络层）和第 7 层（应用层）。</p><ul><li>第 4 层：负载均衡器使用网络层的 TCP 提供的信息。在这一层，它一般不会查看所请求的内容，而是选择一台服务器。</li><li>第 7 层：请求可以根据查询字符串、cookies 或我们选择的任何头的信息，以及包括源和目标地址在内的常规层信息进行均衡。</li></ul><h2 id="_4-扩展关系数据库" tabindex="-1">4 扩展关系数据库 <a class="header-anchor" href="#_4-扩展关系数据库" aria-label="Permalink to &quot;4 扩展关系数据库&quot;">​</a></h2><p>对于一个简单的系统，我们可以通过 RDBMS，如 Oracle 或者 MySQL 来存储数据项。然而，关系数据库系统也存在着一些问题，尤其是在我们需要扩展的时候。</p><p>有很多技术可以扩展关系型数据库：主 - 从复制、主 - 主复制、联合、分片、非规范化和 SQL 调优。</p><ul><li><strong>复制</strong> 通常指的是一种技术，可以让我们在不同的机器上存储同一数据的多个副本。</li><li><strong>联合</strong> （或功能分区）将数据库按功能进行划分。</li><li><strong>分片</strong> 是一种与分区相关的数据库架构模式，它将数据的不同部分放到不同的服务器上，不同的用户将访问数据集的不同部分。</li><li><strong>非规范化</strong> 试图以牺牲一些写入性能为代价来提高读取性能，将数据写入多个表中以避免昂贵的连接。</li><li>SQL 调优。</li></ul><h3 id="主-从复制" tabindex="-1">主 - 从复制 <a class="header-anchor" href="#主-从复制" aria-label="Permalink to &quot;主 - 从复制&quot;">​</a></h3><p>主 - 从复制技术使一个数据库服务器（主服务器）的数据被复制到一个或多个其他数据库服务器（从服务器），如下图所示：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141043849.png" alt="image-20221114104329754" style="zoom:80%;"><p>对主服务器进行的所有更新。</p><ul><li>客户端将连接到主服务器，并更新数据。</li><li>数据随后会在从服务器上进行传输，直到所有的数据在服务器上都是一致的。</li></ul><p>在实践中，还是存在一些瓶颈。</p><ul><li>如果主服务器由于某种原因宕机了，数据仍然可以通过从服务器获得，但是将无法再进行新的写入。</li><li>我们还需要一种新的算法，把一台从服务器提升到主服务器。</li></ul><p>下面是实现仅一台服务器能处理更新请求的一些解决方案。</p><ul><li><strong>同步解决方案</strong> ：只有当所有的服务器都接受了修改数据的事务（分布式事务）之后，才会被提交，因此，当发生故障切换时，数据不会丢失。</li><li><strong>异步解决方案</strong> ：提交 → 延迟 → 传播到集群中的其他服务器，因此，当发生故障切换时，某些数据更新会丢失。</li></ul><p>请记住，如果同步解决方案过慢，那就改成异步解决方案。</p><h3 id="主-主复制" tabindex="-1">主 - 主复制 <a class="header-anchor" href="#主-主复制" aria-label="Permalink to &quot;主 - 主复制&quot;">​</a></h3><p>每个数据库服务器都可以在其他服务器被当作主服务器的同时充当主服务器。在某个时间点上，所有的这服务器都会同步，以确保它们的数据是正确的、最新的。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141043813.png" alt="image-20221114104348737" style="zoom:80%;"><p>所有节点读写所有数据。</p><p>以下是主 - 主复制的一些优势：</p><ul><li>当一台主服务器发生故障时，其他数据库服务器可以正常运行，并接替其工作。当数据库服务器重新上线时，它将利用复制的方式赶上来。</li><li>主服务器可以位于几个物理站点，也可以分布在网络上。</li><li>受限于主服务器处理更新的能力。</li></ul><h3 id="联合" tabindex="-1">联合 <a class="header-anchor" href="#联合" aria-label="Permalink to &quot;联合&quot;">​</a></h3><p>联合（或功能分区）将数据库按功能划分。例如，你可以有三个数据库：Forum、users 和 products，而不是一个单一的单体数据库，这样就能降低对各个数据库的读写流量，因此减少了复制滞后。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141044448.png" alt="image-20221114104412360" style="zoom:80%;"><p>联合按功能划分数据库。</p><p>数据库越小，可以容纳在内存中的数据就越多，这反过来会导致缓存点击率的增加，这是由于缓存命中的改进。因为不需要单一的中央主控器序列化写操作，所以你可以进行并行写入，这样就可以提高吞吐量。</p><h3 id="分片" tabindex="-1">分片 <a class="header-anchor" href="#分片" aria-label="Permalink to &quot;分片&quot;">​</a></h3><p>分片（也被称为数据分区），是一种将大数据库分成许多小部分的技术，这样每个数据库只能管理数据的一个子集。</p><p>在理想情况下，我们有不同的用户都与不同的数据库节点对话。它有助于提高系统的可管理性、性能、可用性和负载均衡。</p><ul><li>每个用户只需要和一个服务器对话，所以可以从该服务器得到快速的响应。</li><li>负载在服务器之间得到了很好的均衡——例如，如果我们有五个服务器，每个服务器只需要处理 20% 的负载。</li></ul><p>在实践中，有许多不同的技术可以将一个数据库分解成多个小部分。</p><h3 id="水平分区" tabindex="-1">水平分区 <a class="header-anchor" href="#水平分区" aria-label="Permalink to &quot;水平分区&quot;">​</a></h3><p>这种技术是将不同的行放到不同的表中。比如，如果我们在一个表中存储用户资料，我们可以决定将 ID 小于 1000 的用户存储在一个表中，而将 ID 大于 1001 小于 2000 的用户存储在另一个表中。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141044806.png" alt="image-20221114104426713" style="zoom:67%;"><p>我们将不同的行放入不同的表中。</p><h3 id="垂直分区" tabindex="-1">垂直分区 <a class="header-anchor" href="#垂直分区" aria-label="Permalink to &quot;垂直分区&quot;">​</a></h3><p>在这种情况下，我们对数据进行划分，将与特定特性相关的表存储在它们自己的服务器上。例如，如果我们正在建立一个类似于 Instagram 的系统——需要存储与用户、他们上传的照片以及他们所关注的人有关的数据——我们可以决定将用户的资料信息放在一台数据库服务器上，好友列表放在另一台服务器上，而照片放在第三台服务器上。</p><p><img src="https://mmbiz.qpic.cn/sz_mmbiz_png/knmrNHnmCLEia2VeobiavVyQyJrdYjibwtib3oopiauSMzWeiajW0icHWdfibp3qKLqVUhA4GBt1A8OyqW1iaylNUDM8zgQ/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片">我们将数据划分，存储与特定特性相关的表，并将其存储在各自的服务器上。</p><h3 id="基于目录的分区" tabindex="-1">基于目录的分区 <a class="header-anchor" href="#基于目录的分区" aria-label="Permalink to &quot;基于目录的分区&quot;">​</a></h3><p>解决这个问题的一个松散耦合的方法，就是创建一个查询服务，它了解你当前的分区模式，并保持每个实体以及存储在哪个数据库分片的映射关系。</p><p>当数据存储可能需要扩展到超出单个存储节点的可用资源时，或者通过减少数据存储中的争用来提高性能时，我们可以使用这种方法。但请记住，分片技术存在以下一些常见问题：</p><ul><li>数据库连接变得更加昂贵，在某些情况下是不可行的。</li><li>分片会破坏数据库的引用完整性。</li><li>数据库模式的改变会变得非常昂贵。</li><li>数据分布不均匀，而且在分片上有大量负载。</li></ul><h3 id="非规范化" tabindex="-1">非规范化 <a class="header-anchor" href="#非规范化" aria-label="Permalink to &quot;非规范化&quot;">​</a></h3><p>非规范化的目的是提高读取性能，但却要牺牲一定的写入性能。为了避免昂贵的连接，可以将数据的冗余副本写入到多个表中。</p><p>一旦数据通过联合和分片等技术变得分散，管理跨数据中心的连接将会进一步增加复杂性。非规范化可以避免需要如此复杂的连接。</p><p>在大多数系统中，读取操作的次数远远多于写入操作，大约是 100:1，甚至是 1000:1。导致读取复杂数据库连接可能会非常昂贵，而且会耗费很多时间在磁盘上。</p><p>有些 RDBMS，像 PostgreSQL 和 Oracle 都支持物化视图，它们可以处理存储冗余数据，并使冗余副本保持一致。</p><p>Facebook 的 Ryan Mack 在其出色的文章《建立时间表：利用非规范化的力量扩大规模来保存你的生活故事》（<em>Building Timeline: Scaling up to hold your life story</em>）中分享了很多时间表自身的实现故事。</p><h2 id="_5-使用哪个数据库" tabindex="-1">5 使用哪个数据库？ <a class="header-anchor" href="#_5-使用哪个数据库" aria-label="Permalink to &quot;5 使用哪个数据库？&quot;">​</a></h2><p>在数据库领域，主要有两种类型的解决方案。SQL 与 NoSQL。它们的构建方式、存储信息的类型以及存储方式都不同</p><h3 id="sql" tabindex="-1">SQL <a class="header-anchor" href="#sql" aria-label="Permalink to &quot;SQL&quot;">​</a></h3><p>关系型数据库以行和列的形式存储数据。每一行包含一个实体的所有信息，每一列包含所有独立的数据点。</p><p>目前最受欢迎的关系型数据库是 MySQL、Oracle、MS SQL Server、SQLite、Postgres 和 MariaDB。</p><h3 id="nosql" tabindex="-1">NoSQL <a class="header-anchor" href="#nosql" aria-label="Permalink to &quot;NoSQL&quot;">​</a></h3><p>它也被称为非关系型数据库。这些数据库一般分为五大类别：Key-Value、Graph、Column、Document 和 Blob 存储。</p><h3 id="键值存储" tabindex="-1">键值存储 <a class="header-anchor" href="#键值存储" aria-label="Permalink to &quot;键值存储&quot;">​</a></h3><p>数据被存储在一个键值对的数组中。<code>key</code>（键）是一个与 <code>value</code>（值）相连的属性名称。</p><p>知名的键值存储有 Redis、Voldemort 和 Dynamo。</p><h3 id="文档数据库" tabindex="-1">文档数据库 <a class="header-anchor" href="#文档数据库" aria-label="Permalink to &quot;文档数据库&quot;">​</a></h3><p>在这些数据库中，数据被存储在文档中（而不是表格中的行和列），这些文档被分组在集合中。每个文档都可能是截然不同的结构。文档数据库包括 CouchDB 和 MongoDB。</p><h3 id="宽列式数据库" tabindex="-1">宽列式数据库 <a class="header-anchor" href="#宽列式数据库" aria-label="Permalink to &quot;宽列式数据库&quot;">​</a></h3><p>在列式数据库中，我们没有“表”，而是有列族，它们是行的容器。与关系型数据库不同，我们不必事先了解所有的列，也不必要求每一行的列数目都相同。</p><p>列式数据库最适合分析大型数据集，著名的有 Cassandra 和 HBase。</p><h3 id="图数据库" tabindex="-1">图数据库 <a class="header-anchor" href="#图数据库" aria-label="Permalink to &quot;图数据库&quot;">​</a></h3><p>这些数据库用于存储数据，其关系最好用图来表示。数据被保存在带有节点（实体）、属性（关于实体的信息）和线（实体之间的连接）的图结构中。</p><p>图数据库的例子包括 Neo4J 和 InfiniteGraph。</p><h3 id="blob-数据库" tabindex="-1">Blob 数据库 <a class="header-anchor" href="#blob-数据库" aria-label="Permalink to &quot;Blob 数据库&quot;">​</a></h3><p>Blob 更像是文件的键 / 值存储，可以通过 Amazon S3、Windows Azure Blob Storage、Google Cloud Storage、Rackspace Cloud Files 或 OpenStack Swift 等 API 访问。</p><p><strong>如何选择要使用的数据库？</strong></p><blockquote><p>当涉及数据库技术时，没有放之四海而皆准的解决方案。这就是为什么许多企业同时依赖 SQL 和 NoSQL 数据库来满足不同的需求。</p></blockquote><p>请看下面我画的思维导图！</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141048164.png" alt="image-20221114104826059" style="zoom:80%;"><p>使用哪个数据库？</p><h2 id="_6-横向扩展-web-层" tabindex="-1">6 横向扩展 Web 层 <a class="header-anchor" href="#_6-横向扩展-web-层" aria-label="Permalink to &quot;6 横向扩展 Web 层&quot;">​</a></h2><p>我们已经扩展了数据层，现在我们也需要扩展 Web 层。为了做到这一点，我们需要将用户会话的数据（状态）移出 Web 层，将其存储在数据库中，如关系型数据库或 NoSQL。这也被称为无状态架构。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141047105.png" alt="image-20221114104749033" style="zoom:80%;"><p>无状态系统很简单。</p><blockquote><p>不要使用有状态架构。</p><p>由于状态的实现会限制可扩展性。降低可用性和提高成本，所以我们需要尽可能地选择无状态架构。</p></blockquote><p>在上面的场景中，由于可以为最优的请求处理选择任意服务器，因此负载均衡器能够可以达到最高的效率。</p><h2 id="_7-先进概念" tabindex="-1">7 先进概念 <a class="header-anchor" href="#_7-先进概念" aria-label="Permalink to &quot;7 先进概念&quot;">​</a></h2><h3 id="缓存-1" tabindex="-1">缓存 <a class="header-anchor" href="#缓存-1" aria-label="Permalink to &quot;缓存&quot;">​</a></h3><p>负载均衡能够帮助你横向扩展越来越多的服务器，但缓存可以让你更好地利用现有的资源，从而更快速地向下一个请求提供数据。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141045038.png" alt="image-20221114104546968" style="zoom:80%;"><p>如果数据不在缓存中，就从数据库中获取，然后保存到缓存中，再从缓存中读取。</p><p>我们可以在服务器中添加缓存，避免从服务器中直接读取网页或数据，从而降低了服务器的响应时间及负载。这使得我们的应用程序更加易于扩展。</p><p>缓存可以被用于许多层，例如数据库层、Web 服务器层和网络层。</p><h3 id="内容分发网络-cdn" tabindex="-1">内容分发网络 (CDN ) <a class="header-anchor" href="#内容分发网络-cdn" aria-label="Permalink to &quot;内容分发网络 (CDN )&quot;">​</a></h3><p>CDN 服务器保存内容（如图像、网页等）的缓存副本，并从最近的位置提供服务。</p><p>CDN 的使用可以提高用户的页面加载时间，因为数据是在离它最近的地方检索的。这也有助于提高内容的可用性，因为它被存储在多个地点。</p><p>使用 CDN 改善了用户的页面加载时间，因为数据是在最接近它的地方被检索到的。</p><p>CDN 服务器向我们的网络服务器发出请求，以验证被缓存的内容，并在需要时更新它们。被缓存的内容通常是静态的，如 HTML 页面、图像、JavaScript 文件、CSS 文件等。</p><h3 id="走向全球" tabindex="-1">走向全球 <a class="header-anchor" href="#走向全球" aria-label="Permalink to &quot;走向全球&quot;">​</a></h3><p>随着你的应用程序在全球范围内推广，你将会在全球范围内建立和运营数据中心，使你的产品每天 24 小时、每周 7 天保持运行。收到的请求将被路由到基于 GeoDNS 的“最佳”数据中心。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141046992.png" alt="image-20221114104649840" style="zoom:80%;"><p>当你的应用程序走向全球时……</p><p>GeoDNS 是一项 DNS 服务，它可以将一个域名按照用户所在的位置解析为 IP 地址。来自亚洲的客户端可以得到与来自欧洲客户端的不同 IP 地址。</p><h3 id="把它整合在一起" tabindex="-1">把它整合在一起 <a class="header-anchor" href="#把它整合在一起" aria-label="Permalink to &quot;把它整合在一起&quot;">​</a></h3><p>通过迭代应用所有这些技术，我们可以轻松地将系统扩展到 1 亿多用户，如无状态架构、应用负载均衡器、尽可能多地使用缓存数据、支持多个数据中心、在 CDN 上托管静态资产、通过分片扩展你的数据层，诸如此类。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211141047432.png" alt="image-20221114104705334" style="zoom:80%;"><p>扩展是一个迭代的过程。</p><h2 id="_8-后面会讨论哪些话题" tabindex="-1">8 后面会讨论哪些话题？ <a class="header-anchor" href="#_8-后面会讨论哪些话题" aria-label="Permalink to &quot;8 后面会讨论哪些话题？&quot;">​</a></h2><p>有很多方法可以提高可扩展性和高性能，如下所示：</p><blockquote><ul><li>分片和复制技术相结合。</li><li>长轮询 vs Websockets vs 服务器发送事件。</li><li>索引和代理。</li><li>SQL 调优。</li><li>弹性计算。</li></ul></blockquote><h1 id="海量请求下的接口并发解决方案" tabindex="-1">海量请求下的接口并发解决方案 <a class="header-anchor" href="#海量请求下的接口并发解决方案" aria-label="Permalink to &quot;海量请求下的接口并发解决方案&quot;">​</a></h1><p>设定一个场景，假如一个商品接口在某段时间突然上升，会怎么办？</p><blockquote><p>生活中的例子来说，假设冰墩墩在当天晚上上热搜之后，迅速有十几万人去淘宝下单购买，此时并没有做好对该商品的缓存预热以及准备，如何操作？</p></blockquote><p>对于这个问题，在电商高并发系统中，对接口的保护一般采用：<strong>缓存、限流、降级</strong> 来操作。</p><p>假设该接口已经接受过风控的处理，过滤掉一半的机器人脚本请求，剩下都是人为的下单请求。</p><h2 id="服务限流" tabindex="-1">服务限流 <a class="header-anchor" href="#服务限流" aria-label="Permalink to &quot;服务限流&quot;">​</a></h2><p>限流 主要的目的是通过对并发访问/请求进行限速，或者对一个时间窗口内的请求进行限速，一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。</p><h3 id="限流算法" tabindex="-1">限流算法 <a class="header-anchor" href="#限流算法" aria-label="Permalink to &quot;限流算法&quot;">​</a></h3><h4 id="_1-漏斗算法" tabindex="-1">1. 漏斗算法 <a class="header-anchor" href="#_1-漏斗算法" aria-label="Permalink to &quot;1. 漏斗算法&quot;">​</a></h4><p>漏桶算法 是当请求到达时直接放入漏桶，如果当前容量已达到上限（限流值），则进行丢弃或其他策略（触发限流策略）。漏桶以固定的速率（根据服务吞吐量）进行释放访问请求（即请求通过），直到漏桶为空。</p><p>漏斗算法的思想就是，不管你来多少请求，我的接口消费速度一定是小于等于流出速率的阈值的。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211221615623.png" alt="image-20221122161515546" style="zoom:80%;"><p>可以基于消息队列来实现。</p><h4 id="_2-令牌桶算法" tabindex="-1">2. 令牌桶算法 <a class="header-anchor" href="#_2-令牌桶算法" aria-label="Permalink to &quot;2. 令牌桶算法&quot;">​</a></h4><p>令牌桶算法 是程序以v（<code>v = 时间周期 / 限流值</code>）的速度向令牌桶中增加令牌，直到令牌桶满，请求到达时向令牌桶请求令牌，如果获取成功则通过请求，如果获取失败触发限流策略。</p><p>令牌桶算法和漏斗算法的思想差别在于，前者可以允许突发请求的发生。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211221615017.png" alt="image-20221122161543938" style="zoom:80%;"><h4 id="_3-滑窗算法" tabindex="-1">3. 滑窗算法 <a class="header-anchor" href="#_3-滑窗算法" aria-label="Permalink to &quot;3. 滑窗算法&quot;">​</a></h4><p>滑窗算法 是将一个时间周期分为N个小周期，分别记录每个小周期内访问次数，并且根据时间滑动删除过期的小周期。</p><p>如下图所示，假设时间周期为1分钟，将1分钟再分为2个小周期，统计每个小周期的访问数量，则可以看到，第一个时间周期内，访问数量为75，第二个时间周期内，访问数量为100，如果一个时间周期内所有的小周期总和超过100的话，则会触发限流策略。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211221615191.png" alt="image-20221122161558122" style="zoom:80%;"><p>Sentinel的实现 和 TCP滑窗。</p><h3 id="接入层限流" tabindex="-1">接入层限流 <a class="header-anchor" href="#接入层限流" aria-label="Permalink to &quot;接入层限流&quot;">​</a></h3><h4 id="nginx限流" tabindex="-1">Nginx限流 <a class="header-anchor" href="#nginx限流" aria-label="Permalink to &quot;Nginx限流&quot;">​</a></h4><p>Nginx 限流采用的是漏桶算法。</p><p>它可以根据客户端特征，限制其访问频率，客户端特征主要指 IP、UserAgent等。使用 IP 比 UserAgent 更可靠，因为 IP 无法造假，UserAgent 可随意伪造。</p><p>limit_req模块基于IP：</p><blockquote><p><a href="http://nginx.org/en/docs/http/ngx_http_limit_req_module.html" target="_blank" rel="noreferrer">http://nginx.org/en/docs/http/ngx_http_limit_req_module.html</a></p></blockquote><p>tgngine：</p><blockquote><p><a href="http://tengine.taobao.org/document_cn/http_limit_req_cn.html" target="_blank" rel="noreferrer">http://tengine.taobao.org/document_cn/http_limit_req_cn.html</a></p></blockquote><h3 id="本地接口限流" tabindex="-1">本地接口限流 <a class="header-anchor" href="#本地接口限流" aria-label="Permalink to &quot;本地接口限流&quot;">​</a></h3><h4 id="semaphore" tabindex="-1">Semaphore <a class="header-anchor" href="#semaphore" aria-label="Permalink to &quot;Semaphore&quot;">​</a></h4><p>Java 并发库 的 Semaphore 可以很轻松完成信号量控制，Semaphore 可以控制某个资源可被同时访问的个数，通过 <code>acquire()</code> 获取一个许可，如果没有就等待，而 <code>release()</code> 释放一个许可。</p><p>假如我们对外提供一个服务接口，允许最大并发数为40，我们可以这样：</p><div class="language-java"><button title="Copy Code" class="copy"></button><span class="lang">java</span><pre class="shiki material-theme-palenight"><code><span class="line"><span style="color:#C792EA;">private</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">final</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">Semaphore</span><span style="color:#A6ACCD;"> permit </span><span style="color:#89DDFF;">=</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">new</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">Semaphore</span><span style="color:#89DDFF;">(</span><span style="color:#F78C6C;">40</span><span style="color:#89DDFF;">,</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">true);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#C792EA;">public</span><span style="color:#A6ACCD;"> </span><span style="color:#C792EA;">void</span><span style="color:#A6ACCD;"> </span><span style="color:#82AAFF;">process</span><span style="color:#89DDFF;">(){</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;font-style:italic;">try</span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        permit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">acquire</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#89DDFF;">        </span><span style="color:#676E95;font-style:italic;">//TODO 处理业务逻辑</span></span>
<span class="line"></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">catch</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">(</span><span style="color:#C792EA;">InterruptedException</span><span style="color:#A6ACCD;"> </span><span style="color:#A6ACCD;font-style:italic;">e</span><span style="color:#89DDFF;">)</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        e</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">printStackTrace</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;font-style:italic;">finally</span><span style="color:#A6ACCD;"> </span><span style="color:#89DDFF;">{</span></span>
<span class="line"><span style="color:#A6ACCD;">        permit</span><span style="color:#89DDFF;">.</span><span style="color:#82AAFF;">release</span><span style="color:#89DDFF;">();</span></span>
<span class="line"><span style="color:#A6ACCD;">    </span><span style="color:#89DDFF;">}</span></span>
<span class="line"><span style="color:#89DDFF;">}</span></span></code></pre></div><p>具体的 Semaphore 实现参考源码。</p><h3 id="分布式接口限流" tabindex="-1">分布式接口限流 <a class="header-anchor" href="#分布式接口限流" aria-label="Permalink to &quot;分布式接口限流&quot;">​</a></h3><h4 id="使用消息队列" tabindex="-1">使用消息队列 <a class="header-anchor" href="#使用消息队列" aria-label="Permalink to &quot;使用消息队列&quot;">​</a></h4><p>不管是用MQ中间件，或是Redis的List实现的消息队列，都可以作为一个 缓冲队列 来使用。思想就是基于漏斗算法。</p><p>当对于一个接口请求达到一定阈值时，就可以启用消息队列来进行接口数据的缓冲，并根据服务的吞吐量来消费数据。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211221616066.png" alt="image-20221122161635995" style="zoom:80%;"><h2 id="服务降级" tabindex="-1">服务降级 <a class="header-anchor" href="#服务降级" aria-label="Permalink to &quot;服务降级&quot;">​</a></h2><p>在接口做好风控的前提下，发现了接口请求的并发量迅速上升，我们可以启用兜底方案，进行服务降级。</p><p>一般服务降级应该用来对一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用。</p><h3 id="降级方案" tabindex="-1">降级方案 <a class="header-anchor" href="#降级方案" aria-label="Permalink to &quot;降级方案&quot;">​</a></h3><h4 id="停止边缘业务" tabindex="-1">停止边缘业务 <a class="header-anchor" href="#停止边缘业务" aria-label="Permalink to &quot;停止边缘业务&quot;">​</a></h4><p>比如淘宝双11前，就不可以查询三个月前的订单，对边缘业务进行降级，保证核心业务的高可用。</p><h4 id="拒绝请求" tabindex="-1">拒绝请求 <a class="header-anchor" href="#拒绝请求" aria-label="Permalink to &quot;拒绝请求&quot;">​</a></h4><p>在接口请求并发量大于阈值，或是接口出现大量失败请求等等突发情况，可以拒绝一些访问请求。</p><h4 id="拒绝策略" tabindex="-1">拒绝策略 <a class="header-anchor" href="#拒绝策略" aria-label="Permalink to &quot;拒绝策略&quot;">​</a></h4><ul><li>随机拒绝：随机拒绝超过阈值的请求 。</li><li>拒绝旧请求：按照请求的时间，优先拒绝更早收到的请求。</li><li>拒绝非核心请求：根据系统业务设置核心请求清单，将非核心清单内的请求拒绝掉。</li></ul><h4 id="恢复方案" tabindex="-1">恢复方案 <a class="header-anchor" href="#恢复方案" aria-label="Permalink to &quot;恢复方案&quot;">​</a></h4><p>在实现服务降级之后，对于突增流量我们可以继续注册多个消费者服务来应对并发量，之后我们再对一些服务器进行慢加载。</p><h2 id="数据缓存" tabindex="-1">数据缓存 <a class="header-anchor" href="#数据缓存" aria-label="Permalink to &quot;数据缓存&quot;">​</a></h2><p>在接口做好风控的前提下，发现了接口请求的并发量迅速上升，我们可以分以下几个操作执行：</p><ul><li>对访问请求使用分布式锁进行阻塞。</li><li>在这个短时间中，我们可以将对应操作行的热点数据，缓存在缓存中间件中。</li><li>放行请求后，让所有请求优先操作缓存数据。</li><li>再将操作的结果通过消息队列发送给消费接口慢慢消费。</li></ul><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211221616881.png" alt="image-20221122161658814" style="zoom:67%;"><h3 id="缓存问题" tabindex="-1">缓存问题 <a class="header-anchor" href="#缓存问题" aria-label="Permalink to &quot;缓存问题&quot;">​</a></h3><p>假设我们操作的是一个库存接口，此时数据库中只有100个库存。</p><p>那假如此时我们将一条数据放入缓存中，如果所有的请求都来访问这个缓存，那它还是被打挂，我们该怎么操作？</p><h4 id="读写分离" tabindex="-1">读写分离 <a class="header-anchor" href="#读写分离" aria-label="Permalink to &quot;读写分离&quot;">​</a></h4><p>第一种想法，读写分离。</p><p>使用Redis的哨兵集群模式来进行主从复制的读写分离操作。读的操作肯定大于写操作，等库存被消费到0时，读操作直接快速失败。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211221617301.png" alt="image-20221122161718229" style="zoom:67%;"><h4 id="负载均衡" tabindex="-1">负载均衡 <a class="header-anchor" href="#负载均衡" aria-label="Permalink to &quot;负载均衡&quot;">​</a></h4><p>第二种想法，负载均衡。</p><p>在缓存数据后，如果所有请求都来缓存中操作这个库存，不管是加悲观锁还是乐观锁，并发率都很低，此时我们可以对这个库存进行拆分。</p><p>我们可以参照 <code>ConcurrentHashMap</code> 中的 <code>counterCells</code> 变量的设计思想，将100个库存拆分到10个缓存服务中，每个缓存服务有10个缓存，然后我们再对请求进行负载均衡到各个缓存服务上。</p><p>但是这种方式会有问题，如果大部分用户被hash到同一个缓存上，导致其他缓存没有被消费，却返回没有库存，这是不合理的。</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2022.12.30/202211221617712.png" alt="image-20221122161737632" style="zoom:67%;"><h4 id="page-cache" tabindex="-1">page cache <a class="header-anchor" href="#page-cache" aria-label="Permalink to &quot;page cache&quot;">​</a></h4><p>第三种想法，page cache。</p><p>大部分软件架构其实都用到了这种方法，比如linux内核的硬盘写入、mysql的刷盘等等，即将短时间内的写操作聚合结果写入，所有的写操作在缓存内完成。</p><h1 id="高并发系统设计的15个锦囊⭐⭐" tabindex="-1">高并发系统设计的15个锦囊⭐⭐ <a class="header-anchor" href="#高并发系统设计的15个锦囊⭐⭐" aria-label="Permalink to &quot;高并发系统设计的15个锦囊⭐⭐&quot;">​</a></h1><p>大家好，我是<strong>田螺</strong>。记得很久之前，去面试过<strong>字节跳动</strong>。被三面的面试官问了一道场景设计题目：<strong>如何设计一个高并发系统</strong>。当时我回答得比较粗糙，最近回想起来，所以整理了设计高并发系统的15个锦囊</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2023.1.30/202301131144167.png" alt="image-20230113114434083" style="zoom:80%;"><h2 id="_1-如何理解高并发系统" tabindex="-1">1. 如何理解高并发系统 <a class="header-anchor" href="#_1-如何理解高并发系统" aria-label="Permalink to &quot;1. 如何理解高并发系统&quot;">​</a></h2><blockquote><p>所谓设计<strong>高并发</strong>系统，就是设计一个系统，保证它<strong>整体可用</strong>的同时，能够<strong>处理很高的并发用户请求</strong>，能够承受<strong>很大的流量冲击</strong>。</p></blockquote><blockquote><p>我们要设计高并发的系统，那就需要处理好一些常见的系统瓶颈问题，如<strong>内存不足、磁盘空间不足，连接数不够，网络宽带不够</strong>等等，以应对突发的流量洪峰。</p></blockquote><h2 id="_1-分而治之-横向扩展" tabindex="-1">1. 分而治之，横向扩展 <a class="header-anchor" href="#_1-分而治之-横向扩展" aria-label="Permalink to &quot;1. 分而治之，横向扩展&quot;">​</a></h2><blockquote><p>如果你<strong>只部署一个应用，只部署一台服务器</strong>，那抗住的流量请求是非常有限的。并且，单体的应用，有单点的风险，如果它挂了，那服务就不可用了。</p></blockquote><blockquote><p>因此，设计一个高并发系统，我们可以<strong>分而治之，横向扩展</strong>。也就是说，采用分布式部署的方式，部署多台服务器，把流量分流开，让每个服务器都承担一部分的并发和流量，提升<strong>整体系统的并发能力</strong>。</p></blockquote><h2 id="_2-微服务拆分-系统拆分" tabindex="-1">2. 微服务拆分（系统拆分） <a class="header-anchor" href="#_2-微服务拆分-系统拆分" aria-label="Permalink to &quot;2. 微服务拆分（系统拆分）&quot;">​</a></h2><blockquote><p>要提高系统的吞吐，提高系统的处理并发请求的能力。除了采用<strong>分布式部署的方式</strong>外，还可以做<strong>微服务拆分</strong>，这样就可以达到分摊请求流量的目的，提高了并发能力。</p></blockquote><blockquote><p>所谓的<strong>微服务拆分</strong>，其实就是把一个单体的应用，按功能单一性，拆分为多个服务模块。<strong>比如一个电商系统，拆分为用户系统、订单系统、商品系统等等</strong>。</p></blockquote><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2023.1.30/202301131148115.png" alt="image-20230113114829040" style="zoom:67%;"><h2 id="_3-分库分表" tabindex="-1">3. 分库分表 <a class="header-anchor" href="#_3-分库分表" aria-label="Permalink to &quot;3. 分库分表&quot;">​</a></h2><blockquote><p>当业务量暴增的话，MySQL单机<strong>磁盘容量会撑爆</strong>。并且，我们知道数据库连接数是有限的。<strong>在高并发的场景下</strong>，大量请求访问数据库，<code>MySQL</code>单机是扛不住的！高并发场景下，会出现<code>too many connections</code>报错。</p></blockquote><blockquote><p>所以高并发的系统，<strong>需要考虑拆分为多个数据库，来抗住高并发的毒打</strong>。而假如你的单表数据量非常大，存储和查询的性能就会遇到瓶颈了，如果你做了很多优化之后还是无法提升效率的时候，就需要考虑做<strong>分表</strong>了。一般千万级别数据量，就需要<strong>分表</strong>，每个表的数据量少一点，提升SQL查询性能。</p></blockquote><blockquote><p>当面试官问要求你设计一个高并发系统的时候，一般都要说到<strong>分库分表</strong>这个点。之前写了分库分表15连问，为了应对面试官追问到底，看我这篇文章：<a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247502983&amp;idx=1&amp;sn=47cc9079b01940cbb83d4f71972e5d20&amp;chksm=cf2213aef8559ab845c5740abc98c335f0b040976bc39781ade95ea085cf32a183021a54ff36&amp;token=1274856030&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">分库分表经典15连问</a></p></blockquote><h2 id="_4-池化技术" tabindex="-1">4. 池化技术 <a class="header-anchor" href="#_4-池化技术" aria-label="Permalink to &quot;4. 池化技术&quot;">​</a></h2><p>在高并发的场景下，<strong>数据库连接数</strong>可能成为瓶颈，因为连接数是有限的。</p><blockquote><p>我们的请求调用数据库时，都会先获取数据库的连接，然后依靠这个连接来查询数据，搞完收工，最后关闭连接，释放资源。如果我们不用数据库连接池的话，每次执行<code>SQL</code>，都要创建连接和销毁连接，这就会导致每个查询请求都变得更慢了，相应的，系统处理用户请求的能力就降低了。</p></blockquote><blockquote><p>因此，需要使用池化技术，即<strong>数据库连接池、HTTP 连接池、Redis 连接池</strong>等等。使用数据库连接池，可以避免每次查询都新建连接，减少不必要的资源开销，通过复用连接池，<strong>提高系统处理高并发请求的能力</strong>。</p></blockquote><blockquote><p>同理，我们使用线程池，也能<strong>让任务并行处理，更高效地完成任务</strong>。大家可以看下我之前线程池的这篇文章，到时候面试官问到这块时，刚好可以扩展开来讲</p></blockquote><ul><li><a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247487945&amp;idx=1&amp;sn=447d2da258797de08eca329a2500d457&amp;chksm=cf21cee0f85647f676dced72811b90bf7db7c898d2a90b7dc2195c5d6279c05d1b125d4b82a1&amp;token=1976733249&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">面试必备：Java线程池解析</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247501030&amp;idx=1&amp;sn=0c0c8523d73d65ba7358856ea02fb5fc&amp;chksm=cf221bcff85592d9556cb3735357b96baad9544c1b9c3149d0bffc290dedab32bb86d40e1075&amp;token=1976733249&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">细数线程池的10个坑</a></li></ul><h2 id="_5-主从分离" tabindex="-1">5. 主从分离 <a class="header-anchor" href="#_5-主从分离" aria-label="Permalink to &quot;5. 主从分离&quot;">​</a></h2><blockquote><p>通常来说，一台单机的MySQL服务器，可以支持<code>500</code>左右的<code>TPS</code>和<code>10000</code>左右的<code>QPS</code>，即单机支撑的<strong>请求访问是有限</strong>的。因此你做了分布式部署，部署了多台机器，部署了主数据库、从数据库。</p></blockquote><blockquote><p>但是，如果双十一搞活动，流量肯定会猛增的。如果所有的查询请求，都走主库的话，主库肯定扛不住，因为查询请求量是非常非常大的。因此一般都要求做<strong>主从分离</strong>，然后实时性要求不高的读请求，都去读从库，<strong>写的请求或者实时性要求高的请求，才走主库</strong>。这样就很好保护了主库，也提高了系统的吞吐。</p></blockquote><blockquote><p>当然，如果回答了主从分离，面试官可能扩展开问你<strong>主从复制原理，问你主从延迟问题</strong>等等，这块大家需要<strong>全方位复习好</strong>哈。可以去看看我之前的这篇文章</p></blockquote><p><a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247497982&amp;idx=1&amp;sn=bb589329cceb5462fc41f66ec63dbf56&amp;chksm=cf2227d7f855aec16dd4d3b3425c0401850eeaf2c9cdc82e82722d38a00c24ee9ccfa3353774&amp;token=1274856030&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">面试必备：聊聊MySQL的主从</a></p><h2 id="_6-使用缓存" tabindex="-1">6. 使用缓存 <a class="header-anchor" href="#_6-使用缓存" aria-label="Permalink to &quot;6. 使用缓存&quot;">​</a></h2><blockquote><p>无论是操作系统，浏览器，还是一些复杂的中间件，你都可以看到缓存的影子。我们使用缓存，主要是提升系统接口的性能，这样高并发场景，你的系统就可以支持更多的用户同时访问。</p></blockquote><blockquote><p>常用的缓存包括：<code>Redis</code>缓存，<code>JVM</code>本地缓存，<code>memcached</code>等等。就拿<code>Redis</code>来说，它单机就能轻轻松松应对几万的并发，你读场景的业务，可以用缓存来抗高并发。</p></blockquote><p>缓存虽然用得爽，但是要<strong>注意缓存使用的一些问题</strong>：</p><blockquote><ul><li>缓存与数据库的一致性问题</li><li>缓存雪崩</li><li>缓存穿透</li><li>缓存击穿</li></ul></blockquote><p>如果大家打算使用<code>Redis</code>的话，需要知道一些注意点，可以看下我之前的这篇文章哈，挺好的。<a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247488325&amp;idx=1&amp;sn=6d9bbe5bf2f2f2904755de5c786fb21b&amp;chksm=cf21cc6cf856457a9d23b3e25ec48107a582e709f05964dfdb5ba77e9a239d8307334c485fdf&amp;token=1371687559&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">使用Redis，你必须知道的21个注意要点</a></p><h2 id="_7-cdn-加速静态资源访问" tabindex="-1">7. CDN，加速静态资源访问 <a class="header-anchor" href="#_7-cdn-加速静态资源访问" aria-label="Permalink to &quot;7. CDN，加速静态资源访问&quot;">​</a></h2><p>商品图片，<code>icon</code>等等静态资源，可以对页面做<strong>静态化处理，减少访问服务端的请求</strong>。如果用户分布在全国各地，有的在上海，有的在深圳，地域相差很远，网速也各不相同。为了让用户最快访问到页面，可以使用<code>CDN</code>。<code>CDN</code>可以让用户就近获取所需内容。</p><p>什么是CDN？</p><blockquote><p>Content Delivery Network/Content Distribution Network,翻译过来就是内容分发网络，它表示将静态资源分发到位于多个地理位置机房的服务器，可以做到数据就近访问，加速了静态资源的访问速度，因此让系统更好处理正常别的动态请求。</p></blockquote><h2 id="_8-消息队列-削锋" tabindex="-1">8. 消息队列，削锋 <a class="header-anchor" href="#_8-消息队列-削锋" aria-label="Permalink to &quot;8. 消息队列，削锋&quot;">​</a></h2><p>我们搞一些双十一、双十二等运营活动时，需要<strong>避免流量暴涨，打垮应用系统的风险</strong>。因此一般会引入消息队列，来应对<strong>高并发的场景</strong>。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_png/PoF8jo1Pmpx3T8Jt9OrdDwwlyKcYzkkSuh3akW0ia8AwSbGk4z4hGSmClKmUg5kFcHaFz0ibHdnGdGt72xSFkQtA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" alt="图片"></p><p>假设你的应用系统每秒最多可以处理<code>2k</code>个请求，每秒却有<code>5k</code>的请求过来，可以引入消息队列，应用系统每秒从消息队列拉<code>2k</code>请求处理得了。</p><p>有些伙伴担心这样可能会出现<strong>消息积压</strong>的问题：</p><ul><li>首先，搞一些运营活动，不会每时每刻都那么多请求过来你的系统（<strong>除非有人恶意攻击</strong>），高峰期过去后，积压的请求可以慢慢处理；</li><li>其次，如果消息队列长度超过最大数量，可以直接抛弃用户请求或跳转到错误页面；</li></ul><h2 id="_9-elasticsearch" tabindex="-1">9. ElasticSearch <a class="header-anchor" href="#_9-elasticsearch" aria-label="Permalink to &quot;9. ElasticSearch&quot;">​</a></h2><blockquote><p><code>Elasticsearch</code>，大家都使用得比较多了吧，<strong>一般搜索功能都会用到它</strong>。它是一个分布式、高扩展、高实时的搜索与数据分析引擎，简称为<code>ES</code>。</p></blockquote><blockquote><p>我们在聊高并发，为啥聊到<code>ES</code>呢？因为<code>ES</code>可以扩容方便，天然支撑高并发。<strong>当数据量大的时候，不用动不动就加机器扩容，分库等等</strong>，可以考虑用<code>ES</code>来支持简单的查询搜索、统计类的操作。</p></blockquote><h2 id="_10-降级熔断" tabindex="-1">10. 降级熔断 <a class="header-anchor" href="#_10-降级熔断" aria-label="Permalink to &quot;10. 降级熔断&quot;">​</a></h2><p><strong>熔断降级</strong>是保护系统的一种手段。当前互联网系统一般都是分布式部署的。而分布式系统中偶尔会出现某个基础服务不可用，最终导致整个系统不可用的情况, 这种现象被称为<strong>服务雪崩效应</strong>。</p><p>比如分布式调用链路<code>A-&gt;B-&gt;C....</code>，下图所示：</p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2023.1.30/202301131147160.png" alt="image-20230113114747071" style="zoom:67%;"><blockquote><p>如果服务<code>C</code>出现问题，比如是因为慢<code>SQL</code>导致调用缓慢，那将导致<code>B</code>也会延迟，从而<code>A</code>也会延迟。堵住的<code>A</code>请求会消耗占用系统的线程、IO、CPU等资源。当请求<code>A</code>的服务越来越多，占用计算机的资源也越来越多，最终会导致系统瓶颈出现，造成其他的请求同样不可用，最后导致业务系统崩溃。</p></blockquote><blockquote><p>为了应对服务雪崩, 常见的做法是<strong>熔断和降级</strong>。最简单是加开关控制，当下游系统出问题时，开关打开降级，不再调用下游系统。还可以选用开源组件<code>Hystrix</code>来支持。设计的系统能应对<strong>高并发场景</strong>，要考虑<strong>熔断降级</strong>逻辑</p></blockquote><h2 id="_11-限流" tabindex="-1">11. 限流 <a class="header-anchor" href="#_11-限流" aria-label="Permalink to &quot;11. 限流&quot;">​</a></h2><p>限流也是我们应对高并发的一种方案。我们当然希望，在高并发大流量过来时，系统能全部请求都正常处理。但是有时候没办法，系统的CPU、网络带宽、内存、线程等资源都是有限的。因此，我们要考虑限流。</p><p>如果你的系统每秒扛住的请求是一千，<strong>如果一秒钟来了十万请求呢</strong>？换个角度就是说，高并发的时候，流量洪峰来了，超过系统的承载能力，怎么办呢？</p><p>这时候，我们可以采取限流方案。就是为了保护系统，多余的请求，直接丢弃。</p><blockquote><p><strong>什么是限流</strong>：在计算机网络中，限流就是控制网络接口发送或接收请求的速率，它可防止DoS攻击和限制Web爬虫。限流，也称流量控制。是指系统在面临高并发，或者大流量请求的情况下，限制新的请求对系统的访问，从而保证系统的稳定性。可以使用<code>Guava</code>的<code>RateLimiter</code>单机版限流，也可以使用<code>Redis</code>分布式限流，还可以使用阿里开源组件<code>sentinel</code>限流。</p></blockquote><blockquote><p>面试的时候，你说到限流这块的话？面试官很大概率会问你限流的算法，因此，大家在准备面试的时候，需要复习一下这几种经典的限流算法哈，可以看下我之前的这篇文章，<a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247490393&amp;idx=1&amp;sn=98189caa486406f8fa94d84ba0667604&amp;chksm=cf21c470f8564d665ce04ccb9dc7502633246da87a0541b07ba4ac99423b28ce544cdd6c036b&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">面试必备：4种经典限流算法讲解</a></p></blockquote><h2 id="_12-异步" tabindex="-1">12. 异步 <a class="header-anchor" href="#_12-异步" aria-label="Permalink to &quot;12. 异步&quot;">​</a></h2><blockquote><p>回忆一下什么是同步，什么是异步呢？以<strong>方法调用</strong>为例，它代表<strong>调用方要阻塞等待被调用方法中的逻辑执行完成</strong>。这种方式下，当被调用方法响应时间较长时，会造成调用方长久的阻塞，在高并发下会造成整体系统性能下降甚至发生雪崩。异步调用恰恰相反，调用方不需要等待方法逻辑执行完成就可以返回执行其他的逻辑，在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方。</p></blockquote><blockquote><p>因此，设计一个高并发的系统，<strong>需要在恰当的场景使用异步</strong>。如何使用异步呢？后端可以借用消息队列实现。比如在海量秒杀请求过来时，先放到消息队列中，快速响应用户，告诉用户请求正在处理中，这样就可以释放资源来处理更多的请求。秒杀请求处理完后，通知用户秒杀抢购成功或者失败。</p></blockquote><h2 id="_13-接口的常规优化" tabindex="-1">13. 接口的常规优化 <a class="header-anchor" href="#_13-接口的常规优化" aria-label="Permalink to &quot;13. 接口的常规优化&quot;">​</a></h2><p>设计一个高并发的系统，需要设计接口的性能足够好，这样系统在相同时间，就可以处理更多的请求。当说到这里的话，可以跟面试官说说接口优化的一些方案了。大家可以看这篇文章哈:<a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247502660&amp;idx=1&amp;sn=17166646f82412cd81955930f799ab4e&amp;chksm=cf22146df8559d7bcf9becd82e1d8006c35a781e5dbd0a79e0a9e121803ee40d6eae7ebd7ccb&amp;token=1371687559&amp;lang=zh_CN&amp;scene=21#wechat_redirect" target="_blank" rel="noreferrer">实战总结！18种接口优化方案的总结</a></p><img src="https://edu-8673.oss-cn-beijing.aliyuncs.com/img2023.1.30/202301131146489.png" alt="image-20230113114635398" style="zoom:80%;"><h2 id="_14-压力测试确定系统瓶颈" tabindex="-1">14. 压力测试确定系统瓶颈 <a class="header-anchor" href="#_14-压力测试确定系统瓶颈" aria-label="Permalink to &quot;14. 压力测试确定系统瓶颈&quot;">​</a></h2><blockquote><p>设计高并发系统，离不开最重要的一环，<strong>就是压力测试</strong>。就是在系统上线前，需要对系统进行压力测试，测清楚你的系统支撑的最大并发是多少，确定系统的瓶颈点，让自己心里有底，最好预防措施。</p></blockquote><blockquote><p>压测完要分析整个调用链路，性能可能出现问题是网络层（如带宽）、Nginx层、服务层、还是数据路缓存等中间件等等。<code>loadrunner</code>是一款不错的压力测试工具，<code>jmeter</code>则是接口性能测试工具，都可以来做下压测。</p></blockquote><h2 id="_15-应对突发流量峰值-扩容-切流量" tabindex="-1">15. 应对突发流量峰值：扩容+切流量 <a class="header-anchor" href="#_15-应对突发流量峰值-扩容-切流量" aria-label="Permalink to &quot;15. 应对突发流量峰值：扩容+切流量&quot;">​</a></h2><p>如果是突发的流量高峰，除了降级、限流保证系统不跨，我们可以采用这两种方案，保证系统尽可能服务用户请求：</p><blockquote><ul><li>扩容：<strong>比如增加从库、提升配置的方式</strong>，提升系统/组件的流量承载能力。比如增加<code>MySQL、Redis</code>从库来处理查询请求。</li><li>切流量：<strong>服务多机房部署</strong>，如果高并发流量来了，把流量从一个机房切换到另一个机房。</li></ul></blockquote></div></div></main><footer class="VPDocFooter" data-v-6b87e69f data-v-37656e44><!--[--><!--]--><!----><nav class="prev-next" data-v-37656e44><div class="pager" data-v-37656e44><a class="pager-link prev" href="/notebook/%E4%B8%89%E9%AB%98/%E9%AB%98%E5%8F%AF%E7%94%A8.html" data-v-37656e44><span class="desc" data-v-37656e44>Previous page</span><span class="title" data-v-37656e44>高可用</span></a></div><div class="pager" data-v-37656e44><a class="pager-link next" href="/notebook/%E4%BA%91%E5%8E%9F%E7%94%9F/Docker.html" data-v-37656e44><span class="desc" data-v-37656e44>Next page</span><span class="title" data-v-37656e44>Docker</span></a></div></nav></footer><!--[--><!--]--></div></div></div><!--[--><!--]--></div></div><!----><!--[--><!--]--></div></div>
    <script>window.__VP_HASH_MAP__=JSON.parse("{\"2、数据库_mysql_mysql面试_基础.md\":\"40da680a\",\"1、学前端_5、小程序_小程序项目.md\":\"60a1629b\",\"1、学前端_4、node_知识篇.md\":\"a7fb500e\",\"1、学前端_2、js_ts_es6 进阶.md\":\"6d07ba10\",\"1、学前端_3、vue_vue3_vue3进阶.md\":\"7ac622b4\",\"5、运维_jenkins.md\":\"929081f8\",\"1、学前端_2、js_ts_typescript.md\":\"875a4aa4\",\"2、数据库_mysql_mysql核心_设计.md\":\"7faf46d1\",\"2、数据库_mysql_mysql核心_基础.md\":\"d8e97f3e\",\"1、学前端_1、html_css_html基础.md\":\"7584d076\",\"1、学前端_5、专题篇_问题篇.md\":\"e893aaa2\",\"2、数据库_mysql_mysql面试_进阶.md\":\"f934806d\",\"3、springboot_运维_原理.md\":\"f4a39db6\",\"2、数据库_influxdb.md\":\"6e1711e1\",\"3、springboot_新特性.md\":\"cdf3e307\",\"mybatis_mybatisplus_jpa.md\":\"8e41681b\",\"1、学前端_5、小程序_小程序优化.md\":\"a2185198\",\"2、数据库_redis_redis基础.md\":\"856df0e0\",\"linux_实用脚本.md\":\"f2299dd5\",\"4、微服务_必备_分布式基础.md\":\"d49863d5\",\"2、数据库_redis_redis优化.md\":\"e66ae32f\",\"4、微服务_springsecurity_进阶篇.md\":\"235a8e9e\",\"5、运维_chatgpt.md\":\"10db3823\",\"2、数据库_mysql_分库分表.md\":\"e1c8a095\",\"start.md\":\"9bc1ff8d\",\"5、运维_github.md\":\"2ec6c735\",\"java学前端_css.md\":\"f11b47f0\",\"1、学前端_5、专题篇_知识篇.md\":\"a463ed8d\",\"linux_软件部署.md\":\"d6722925\",\"2、数据库_neo4j.md\":\"97ad22ac\",\"team.md\":\"ce467a6a\",\"nginx_实战篇.md\":\"7785486e\",\"index.md\":\"8c3ec167\",\"计算机基础_计算机网络_网络基础.md\":\"7a54a85d\",\"1、学前端_4、node_进阶篇.md\":\"60f6db69\",\"java_java集合.md\":\"a049b313\",\"1、学前端_3、vue_vue3_vue3高级.md\":\"614d1516\",\"1、学前端_5、小程序_微信小程序.md\":\"9a4be771\",\"5、运维_netty.md\":\"12ca0278\",\"2、数据库_mysql_mysql核心_运维.md\":\"83f97c16\",\"idea_vs code.md\":\"afdcb593\",\"java学前端_vue3_组件.md\":\"1086884e\",\"idea_chrome.md\":\"4a32afbc\",\"云原生_k8s.md\":\"db58e65a\",\"2、数据库_mysql_mysql核心_进阶.md\":\"61d16dff\",\"ssm_springbatch.md\":\"f799ab4a\",\"三高_分布式.md\":\"db1b8a1b\",\"2、数据库_elasticsearch_1、es基础.md\":\"04d17448\",\"linux_linux基础.md\":\"4b0bf394\",\"idea_idea插件.md\":\"fa86e45a\",\"可视化 _ 监控_可视化大屏.md\":\"004553bd\",\"2、数据库_mongodb_整合.md\":\"3c47d7f4\",\"4、微服务_springsecurity_基础篇.md\":\"534a3401\",\"4、微服务_进阶.md\":\"69095c58\",\"计算机基础_计算机基础_操作系统.md\":\"0f75d113\",\"可视化 _ 监控_zabbix.md\":\"71f2270e\",\"nginx_基础篇.md\":\"c7d8bb50\",\"1、学前端_4、node_项目实战.md\":\"bc5065b8\",\"2、数据库_redis_redis原理.md\":\"5cedf685\",\"可视化 _ 监控_监控基础.md\":\"ac56ce4d\",\"三高_高并发.md\":\"ea9ffc99\",\"2、数据库_redis_redis高级.md\":\"1d5872f6\",\"1、学前端_4、node_基础篇.md\":\"581cc13a\",\"2、数据库_mongodb_基础.md\":\"fb7a0a29\",\"idea_idea基础.md\":\"6f2f9638\",\"4、微服务_必备_sentinel.md\":\"2edfbf6c\",\"2、数据库_elasticsearch_3、es高级.md\":\"ef146606\",\"1、学前端_3、vue_vue3_vue3新语法.md\":\"8afd5409\",\"消息中间件_canal.md\":\"3949163c\",\"ssm_maven.md\":\"2c5e12ed\",\"4、微服务_springsecurity_高级篇.md\":\"882d3ff3\",\"linux_linux进阶.md\":\"188ef7b4\",\"计算机基础_设计模式_uml.md\":\"634ba256\",\"计算机基础_算法_leetcode.md\":\"77162fb9\",\"项目实战_小兔鲜_进阶篇1.md\":\"17c52c81\",\"1、学前端_2、js_ts_es6 基础.md\":\"fda3f18b\",\"项目实战_小兔鲜_进阶篇2.md\":\"a0f23006\",\"软件测试_测试基础.md\":\"8c1060cd\",\"2、数据库_redis_本地缓存.md\":\"00617fe6\",\"nginx_面试篇.md\":\"e3fb373a\",\"mybatis_mybatisplus_mybatis.md\":\"9239e0ad\",\"linux_shell.md\":\"ae53d83b\",\"2、数据库_mysql_mysql核心_优化.md\":\"36230425\",\"项目实战_项目推荐.md\":\"f9d97630\",\"mybatis_mybatisplus_mybatisplus.md\":\"0030fd35\",\"项目实战_百度地图_进阶篇.md\":\"c8b93267\",\"三高_高可用.md\":\"323840c5\",\"java_java新特性.md\":\"22abf56d\",\"软件测试_压力测试.md\":\"9ab44440\",\"java学前端_html_js.md\":\"e0fcd240\",\"2、数据库_redis_redis实战.md\":\"d6daeeab\",\"nginx_进阶篇.md\":\"e6b63195\",\"三高_秒杀.md\":\"3878bb64\",\"5、运维_git.md\":\"0264925c\",\"java_java进阶.md\":\"e79cb5b4\",\"并发 _ 多线程_基础篇.md\":\"7adbfac5\",\"项目实战_百度地图_基础篇.md\":\"8afa5954\",\"java学前端_react.md\":\"3ec827dd\",\"1、学前端_1、html_css_css基础.md\":\"01b56712\",\"项目实战_小兔鲜_基础篇.md\":\"646f5df5\",\"1、学前端_2、js_ts_js 基础.md\":\"cb13e36f\",\"可视化 _ 监控_监控进阶.md\":\"0cdbc292\",\"计算机基础_设计模式_基础篇.md\":\"51617287\",\"计算机基础_数据结构_基础篇.md\":\"b2bfd8d4\",\"项目实战_苍穹外卖_进阶篇.md\":\"48415e41\",\"ssm_spring.md\":\"ab514659\",\"消息中间件_rabbitmq.md\":\"45b1eb28\",\"1、学前端_1、html_css_网页进阶.md\":\"db998248\",\"消息中间件_kafka.md\":\"b747dabf\",\"云原生_docker.md\":\"983c7ba7\",\"4、微服务_必备_分布式锁.md\":\"5af1cf8d\",\"消息中间件_rocketmq.md\":\"d441da85\",\"项目实战_黑马头条_基础篇.md\":\"b05af3a6\",\"ssm_springmvc.md\":\"81b9714f\",\"项目实战_支付.md\":\"1d7407dd\",\"项目实战_黑马头条_进阶篇2.md\":\"bff0015b\",\"项目实战_黑马头条_进阶篇.md\":\"19f18388\",\"java学前端_vue2_组件.md\":\"58c6b1df\",\"3、springboot_基础篇.md\":\"529c66f4\",\"3、springboot_应用篇.md\":\"8b92aa61\",\"项目实战_黑马头条_高级篇.md\":\"227c08c1\",\"1、学前端_5、小程序_uniapp.md\":\"71a282b4\",\"项目实战_云尚办公_基础篇.md\":\"1fe188ba\",\"并发 _ 多线程_并发完善.md\":\"26619c46\",\"1、学前端_2、js_ts_js 进阶.md\":\"657dfb8f\",\"java_java高级.md\":\"23782d1a\",\"java_java基础.md\":\"86d67c77\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"VitePress\",\"description\":\"A VitePress site\",\"base\":\"/notebook/\",\"head\":[],\"appearance\":true,\"themeConfig\":{\"algolia\":{\"appId\":\"DW7O63I9IR\",\"apiKey\":\"f8ed758cdb288a8b06542bc35923c1a1\",\"indexName\":\"notebook\"},\"sidebar\":[{\"text\":\"Java\",\"collapsed\":true,\"items\":[{\"text\":\"Java基础\",\"link\":\"/Java/Java基础\"},{\"text\":\"Java新特性\",\"link\":\"/Java/Java新特性\"},{\"text\":\"Java进阶\",\"link\":\"/Java/Java进阶\"},{\"text\":\"Java集合\",\"link\":\"/Java/Java集合\"},{\"text\":\"Java高级\",\"link\":\"/Java/Java高级\"}]},{\"text\":\"Linux\",\"collapsed\":true,\"items\":[{\"text\":\"Linux基础\",\"link\":\"/Linux/Linux基础\"},{\"text\":\"Linux新特性\",\"link\":\"/Linux/Linux进阶\"},{\"text\":\"Shell脚本\",\"link\":\"/Linux/Shell\"},{\"text\":\"实用脚本\",\"link\":\"/Linux/实用脚本\"},{\"text\":\"软件部署\",\"link\":\"/Linux/软件部署\"}]},{\"text\":\"Nginx\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/Nginx/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/Nginx/进阶篇\"},{\"text\":\"实战篇\",\"link\":\"/Nginx/实战篇\"},{\"text\":\"面试篇\",\"link\":\"/Nginx/面试篇\"}]},{\"text\":\"SSM\",\"collapsed\":true,\"items\":[{\"text\":\"Maven\",\"link\":\"/SSM/Maven\"},{\"text\":\"Spring\",\"link\":\"/SSM/Spring\"},{\"text\":\"SpringMVC\",\"link\":\"/SSM/SpringMVC\"},{\"text\":\"SpringBatch\",\"link\":\"/SSM/SpringBatch\"}]},{\"text\":\"SpringBoot\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/3、SpringBoot/基础篇\"},{\"text\":\"应用篇\",\"link\":\"/3、SpringBoot/应用篇\"},{\"text\":\"新特性\",\"link\":\"/3、SpringBoot/新特性\"},{\"text\":\"运维&原理\",\"link\":\"/3、SpringBoot/运维&原理\"}]},{\"text\":\"SpringCloud\",\"collapsed\":true,\"items\":[{\"text\":\"SpringCloud\",\"link\":\"/4、微服务/进阶\"},{\"text\":\"Sentinel\",\"link\":\"/4、微服务/必备/Sentinel\"}]},{\"text\":\"SpringSecurity\",\"collapsed\":true,\"items\":[{\"text\":\"SpringSecurity基础篇\",\"link\":\"/4、微服务/SpringSecurity/基础篇\"},{\"text\":\"SpringSecurity进阶篇\",\"link\":\"/4、微服务/SpringSecurity/进阶篇\"},{\"text\":\"SpringSecurity高级篇\",\"link\":\"/4、微服务/SpringSecurity/高级篇\"}]},{\"text\":\"Mybatis & MybatisPlus\",\"collapsed\":true,\"items\":[{\"text\":\"Mybatis\",\"link\":\"/Mybatis&MybatisPlus/Mybatis\"},{\"text\":\"MybatisPlus\",\"link\":\"/Mybatis&MybatisPlus/MybatisPlus\"},{\"text\":\"JPA\",\"link\":\"/Mybatis&MybatisPlus/JPA\"}]},{\"text\":\"Git & ChatGPT\",\"collapsed\":true,\"items\":[{\"text\":\"Git\",\"link\":\"/5、运维/Git\"},{\"text\":\"Github\",\"link\":\"/5、运维/Github\"},{\"text\":\"ChatGPT\",\"link\":\"/5、运维/ChatGPT\"},{\"text\":\"Jenkins\",\"link\":\"/5、运维/Jenkins\"},{\"text\":\"Netty\",\"link\":\"/5、运维/Netty\"}]},{\"text\":\"数据库\",\"collapsed\":true,\"items\":[{\"text\":\"MySQL\",\"collapsed\":true,\"items\":[{\"text\":\"MySQL基础\",\"link\":\"/2、数据库/MySQL/MySQL核心/基础\"},{\"text\":\"MySQL进阶\",\"link\":\"/2、数据库/MySQL/MySQL核心/进阶\"},{\"text\":\"MySQL优化\",\"link\":\"/2、数据库/MySQL/MySQL核心/优化\"},{\"text\":\"MySQL设计\",\"link\":\"/2、数据库/MySQL/MySQL核心/设计\"},{\"text\":\"MySQL运维\",\"link\":\"/2、数据库/MySQL/MySQL核心/运维\"},{\"text\":\"分库分表\",\"link\":\"/2、数据库/MySQL/分库分表\"}]},{\"text\":\"Redis\",\"collapsed\":true,\"items\":[{\"text\":\"Redis基础\",\"link\":\"/2、数据库/Redis/Redis基础\"},{\"text\":\"Redis优化\",\"link\":\"/2、数据库/Redis/Redis优化\"},{\"text\":\"Redis原理\",\"link\":\"/2、数据库/Redis/Redis原理\"},{\"text\":\"Redis高级\",\"link\":\"/2、数据库/Redis/Redis高级\"},{\"text\":\"Redis实战\",\"link\":\"/2、数据库/Redis/Redis实战\"},{\"text\":\"本地缓存\",\"link\":\"/2、数据库/Redis/本地缓存\"}]},{\"text\":\"MongoDB\",\"collapsed\":true,\"items\":[{\"text\":\"MongoDB基础\",\"link\":\"/2、数据库/MongoDB/基础\"},{\"text\":\"MongoDB进阶\",\"link\":\"/2、数据库/MongoDB/整合\"}]},{\"text\":\"ElasticSearch\",\"collapsed\":true,\"items\":[{\"text\":\"ES基础\",\"link\":\"/2、数据库/ElasticSearch/1、ES基础\"},{\"text\":\"ES高级\",\"link\":\"/2、数据库/ElasticSearch/3、ES高级\"}]},{\"text\":\"InfluxDB\",\"link\":\"/2、数据库/influxdb\"},{\"text\":\"Neo4j\",\"link\":\"/2、数据库/Neo4j\"}]},{\"text\":\"高并发 & 秒杀 & 分布式\",\"collapsed\":true,\"items\":[{\"text\":\"分布式理论\",\"link\":\"/三高/分布式\"},{\"text\":\"分布式锁\",\"link\":\"/4、微服务/必备/分布式锁\"},{\"text\":\"秒杀\",\"link\":\"/三高/秒杀\"},{\"text\":\"高可用\",\"link\":\"/三高/高可用\"},{\"text\":\"高并发\",\"link\":\"/三高/高并发\"}]},{\"text\":\"云原生\",\"collapsed\":true,\"items\":[{\"text\":\"Docker\",\"link\":\"/云原生/Docker\"},{\"text\":\"K8S\",\"link\":\"/云原生/K8S\"}]},{\"text\":\"可视化 & 监控\",\"collapsed\":true,\"items\":[{\"text\":\"监控基础\",\"link\":\"/可视化 & 监控/监控基础\"},{\"text\":\"监控进阶\",\"link\":\"/可视化 & 监控/监控进阶\"},{\"text\":\"可视化大屏\",\"link\":\"/可视化 & 监控/可视化大屏\"},{\"text\":\"Zabbix\",\"link\":\"/可视化 & 监控/Zabbix\"}]},{\"text\":\"学前端\",\"collapsed\":true,\"items\":[{\"text\":\"HTML+CSS\",\"collapsed\":true,\"items\":[{\"text\":\"HTML基础\",\"link\":\"/1、学前端/1、HTML+CSS/HTML基础\"},{\"text\":\"CSS基础\",\"link\":\"/1、学前端/1、HTML+CSS/CSS基础\"},{\"text\":\"网页进阶\",\"link\":\"/1、学前端/1、HTML+CSS/网页进阶\"}]},{\"text\":\"JS+TS\",\"collapsed\":true,\"items\":[{\"text\":\"JS基础\",\"link\":\"/1、学前端/2、JS+TS/JS 基础\"},{\"text\":\"JS进阶\",\"link\":\"/1、学前端/2、JS+TS/JS 进阶\"},{\"text\":\"ES6基础\",\"link\":\"/1、学前端/2、JS+TS/ES6 基础\"},{\"text\":\"ES6进阶\",\"link\":\"/1、学前端/2、JS+TS/ES6 进阶\"},{\"text\":\"TS基础\",\"link\":\"/1、学前端/2、JS+TS/TypeScript\"}]},{\"text\":\"NodeJS\",\"collapsed\":true,\"items\":[{\"text\":\"Node基础\",\"link\":\"/1、学前端/4、Node/基础篇\"},{\"text\":\"Node进阶\",\"link\":\"/1、学前端/4、Node/进阶篇\"},{\"text\":\"项目实战\",\"link\":\"/1、学前端/4、Node/项目实战\"}]},{\"text\":\"Vue\",\"collapsed\":true,\"items\":[{\"text\":\"Vue3进阶\",\"link\":\"/1、学前端/3、Vue/Vue3/Vue3进阶\"},{\"text\":\"Vue3高级\",\"link\":\"/1、学前端/3、Vue/Vue3/Vue3高级\"},{\"text\":\"Vue3新语法\",\"link\":\"/1、学前端/3、Vue/Vue3/Vue3新语法\"},{\"text\":\"项目实战\",\"link\":\"/1、学前端/3、Vue/Vue2/Vue2项目\"}]},{\"text\":\"小程序\",\"collapsed\":true,\"items\":[{\"text\":\"小程序基础\",\"link\":\"/1、学前端/5、小程序/微信小程序\"},{\"text\":\"小程序优化\",\"link\":\"/1、学前端/5、小程序/小程序优化\"},{\"text\":\"uniapp\",\"link\":\"/1、学前端/5、小程序/uniapp\"},{\"text\":\"项目实战\",\"link\":\"/1、学前端/5、小程序/小程序项目\"}]}]},{\"text\":\"计算机基础\",\"collapsed\":true,\"items\":[{\"text\":\"数据结构\",\"link\":\"/计算机基础/数据结构/基础篇\"},{\"text\":\"操作系统\",\"link\":\"/计算机基础/计算机基础/操作系统\"},{\"text\":\"设计模式\",\"link\":\"/计算机基础/设计模式/基础篇\"},{\"text\":\"计算机网络\",\"link\":\"/计算机基础/计算机网络/网络基础\"},{\"text\":\"UML\",\"link\":\"/计算机基础/设计模式/UML\"},{\"text\":\"LeetCode\",\"link\":\"/计算机基础/算法/LeetCode\"}]},{\"text\":\"项目实战\",\"collapsed\":true,\"items\":[{\"text\":\"云尚办公\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/云尚办公/基础篇\"}]},{\"text\":\"小兔鲜\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/小兔鲜/基础篇\"},{\"text\":\"进阶篇1\",\"link\":\"/项目实战/小兔鲜/进阶篇1\"},{\"text\":\"进阶篇2\",\"link\":\"/项目实战/小兔鲜/进阶篇2\"}]},{\"text\":\"地图\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/百度地图/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/项目实战/百度地图/进阶篇\"}]},{\"text\":\"苍穹外卖\",\"collapsed\":true,\"items\":[{\"text\":\"进阶篇\",\"link\":\"/项目实战/苍穹外卖/进阶篇\"}]},{\"text\":\"黑马头条\",\"collapsed\":true,\"items\":[{\"text\":\"基础篇\",\"link\":\"/项目实战/黑马头条/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/项目实战/黑马头条/进阶篇\"},{\"text\":\"进阶篇2\",\"link\":\"/项目实战/黑马头条/进阶篇2\"},{\"text\":\"高级篇\",\"link\":\"/项目实战/黑马头条/高级篇\"}]},{\"text\":\"支付\",\"link\":\"/项目实战/支付\"},{\"text\":\"项目推荐\",\"link\":\"/项目实战/项目推荐\"}]},{\"text\":\"团队成员\",\"link\":\"/team\"}],\"siteTitle\":\"任硕的文档\",\"logo\":\"/Vue.png\",\"nav\":[{\"text\":\"Java学前端\",\"items\":[{\"items\":[{\"text\":\"HTML+JS\",\"link\":\"/Java学前端/HTML+JS\"},{\"text\":\"CSS\",\"link\":\"/Java学前端/CSS\"},{\"text\":\"Vue2+组件\",\"link\":\"/Java学前端/Vue2+组件\"},{\"text\":\"Vue3+组件\",\"link\":\"/Java学前端/Vue3+组件\"},{\"text\":\"React\",\"link\":\"/Java学前端/React\"}]}],\"activeMatch\":\"/Java/\"},{\"text\":\"软件测试\",\"items\":[{\"items\":[{\"text\":\"测试基础\",\"link\":\"/软件测试/测试基础\"},{\"text\":\"压力测试\",\"link\":\"/软件测试/压力测试\"}]}]},{\"text\":\"多线程\",\"items\":[{\"items\":[{\"text\":\"基础篇\",\"link\":\"/并发 & 多线程/基础篇\"},{\"text\":\"进阶篇\",\"link\":\"/并发 & 多线程/并发完善\"}]}]},{\"text\":\"开发工具\",\"items\":[{\"items\":[{\"text\":\"Chrome\",\"link\":\"/IDEA/Chrome\"},{\"text\":\"IDEA基础\",\"link\":\"/IDEA/IDEA基础\"},{\"text\":\"IDEA插件\",\"link\":\"/IDEA/IDEA插件\"},{\"text\":\"VS Code\",\"link\":\"/IDEA/VS Code\"}]}]},{\"text\":\"消息中间件\",\"items\":[{\"items\":[{\"text\":\"RabbitMQ\",\"link\":\"/消息中间件/RabbitMQ\"},{\"text\":\"RocketMQ\",\"link\":\"/消息中间件/RocketMQ\"},{\"text\":\"Kafka\",\"link\":\"/消息中间件/Kafka\"},{\"text\":\"Canal\",\"link\":\"/消息中间件/Canal\"}]}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/renshuo123/renshuo123.github.io\"},{\"icon\":\"twitter\",\"link\":\"#\"},{\"icon\":{\"svg\":\"<svg t=\\\"1676028692954\\\" class=\\\"icon\\\" ...</path></svg>\"},\"link\":\"https://github.com/\"}]},\"locales\":{},\"scrollOffset\":90,\"cleanUrls\":false}");</script>
    
  </body>
</html>